about summary refs log tree commit diff
path: root/src/view/com/lists
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/lists')
-rw-r--r--src/view/com/lists/ListCard.tsx6
-rw-r--r--src/view/com/lists/ListMembers.tsx (renamed from src/view/com/lists/ListItems.tsx)114
-rw-r--r--src/view/com/lists/MyLists.tsx (renamed from src/view/com/lists/ListsList.tsx)90
-rw-r--r--src/view/com/lists/ProfileLists.tsx226
4 files changed, 319 insertions, 117 deletions
diff --git a/src/view/com/lists/ListCard.tsx b/src/view/com/lists/ListCard.tsx
index a481902d8..774e9e916 100644
--- a/src/view/com/lists/ListCard.tsx
+++ b/src/view/com/lists/ListCard.tsx
@@ -7,7 +7,7 @@ import {RichText as RichTextCom} from '../util/text/RichText'
 import {UserAvatar} from '../util/UserAvatar'
 import {s} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
+import {useSession} from '#/state/session'
 import {sanitizeDisplayName} from 'lib/strings/display-names'
 import {sanitizeHandle} from 'lib/strings/handles'
 import {makeProfileLink} from 'lib/routes/links'
@@ -28,7 +28,7 @@ export const ListCard = ({
   style?: StyleProp<ViewStyle>
 }) => {
   const pal = usePalette('default')
-  const store = useStores()
+  const {currentAccount} = useSession()
 
   const rkey = React.useMemo(() => {
     try {
@@ -80,7 +80,7 @@ export const ListCard = ({
             {list.purpose === 'app.bsky.graph.defs#modlist' &&
               'Moderation list '}
             by{' '}
-            {list.creator.did === store.me.did
+            {list.creator.did === currentAccount?.did
               ? 'you'
               : sanitizeHandle(list.creator.handle, '@')}
           </Text>
diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListMembers.tsx
index 192cdd9d3..e6afb3d3c 100644
--- a/src/view/com/lists/ListItems.tsx
+++ b/src/view/com/lists/ListMembers.tsx
@@ -1,6 +1,7 @@
 import React, {MutableRefObject} from 'react'
 import {
   ActivityIndicator,
+  Dimensions,
   RefreshControl,
   StyleProp,
   View,
@@ -8,27 +9,28 @@ import {
 } from 'react-native'
 import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
 import {FlatList} from '../util/Views'
-import {observer} from 'mobx-react-lite'
 import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
 import {ProfileCard} from '../profile/ProfileCard'
 import {Button} from '../util/forms/Button'
-import {ListModel} from 'state/models/content/list'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {s} from 'lib/styles'
-import {OnScrollCb} from 'lib/hooks/useOnMainScroll'
+import {useListMembersQuery} from '#/state/queries/list-members'
+import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
 import {logger} from '#/logger'
+import {useModalControls} from '#/state/modals'
+import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
+import {useSession} from '#/state/session'
+import {cleanError} from '#/lib/strings/errors'
 
 const LOADING_ITEM = {_reactKey: '__loading__'}
 const EMPTY_ITEM = {_reactKey: '__empty__'}
 const ERROR_ITEM = {_reactKey: '__error__'}
 const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
 
-export const ListItems = observer(function ListItemsImpl({
+export function ListMembers({
   list,
   style,
   scrollElRef,
@@ -41,10 +43,10 @@ export const ListItems = observer(function ListItemsImpl({
   headerOffset = 0,
   desktopFixedHeightOffset,
 }: {
-  list: ListModel
+  list: string
   style?: StyleProp<ViewStyle>
   scrollElRef?: MutableRefObject<FlatList<any> | null>
-  onScroll?: OnScrollCb
+  onScroll: OnScrollHandler
   onPressTryAgain?: () => void
   renderHeader: () => JSX.Element
   renderEmptyState: () => JSX.Element
@@ -54,37 +56,47 @@ export const ListItems = observer(function ListItemsImpl({
   desktopFixedHeightOffset?: number
 }) {
   const pal = usePalette('default')
-  const store = useStores()
   const {track} = useAnalytics()
   const [isRefreshing, setIsRefreshing] = React.useState(false)
   const {isMobile} = useWebMediaQueries()
+  const {openModal} = useModalControls()
+  const {currentAccount} = useSession()
 
-  const data = React.useMemo(() => {
+  const {
+    data,
+    isFetching,
+    isFetched,
+    isError,
+    error,
+    refetch,
+    fetchNextPage,
+    hasNextPage,
+  } = useListMembersQuery(list)
+  const isEmpty = !isFetching && !data?.pages[0].items.length
+  const isOwner =
+    currentAccount && data?.pages[0].list.creator.did === currentAccount.did
+
+  const items = React.useMemo(() => {
     let items: any[] = []
-    if (list.hasLoaded) {
-      if (list.hasError) {
+    if (isFetched) {
+      if (isEmpty && isError) {
         items = items.concat([ERROR_ITEM])
       }
-      if (list.isEmpty) {
+      if (isEmpty) {
         items = items.concat([EMPTY_ITEM])
-      } else {
-        items = items.concat(list.items)
+      } else if (data) {
+        for (const page of data.pages) {
+          items = items.concat(page.items)
+        }
       }
-      if (list.loadMoreError) {
+      if (!isEmpty && isError) {
         items = items.concat([LOAD_MORE_ERROR_ITEM])
       }
-    } else if (list.isLoading) {
+    } else if (isFetching) {
       items = items.concat([LOADING_ITEM])
     }
     return items
-  }, [
-    list.hasError,
-    list.hasLoaded,
-    list.isLoading,
-    list.isEmpty,
-    list.items,
-    list.loadMoreError,
-  ])
+  }, [isFetched, isEmpty, isError, data, isFetching])
 
   // events
   // =
@@ -93,45 +105,36 @@ export const ListItems = observer(function ListItemsImpl({
     track('Lists:onRefresh')
     setIsRefreshing(true)
     try {
-      await list.refresh()
+      await refetch()
     } catch (err) {
       logger.error('Failed to refresh lists', {error: err})
     }
     setIsRefreshing(false)
-  }, [list, track, setIsRefreshing])
+  }, [refetch, track, setIsRefreshing])
 
   const onEndReached = React.useCallback(async () => {
+    if (isFetching || !hasNextPage || isError) return
     track('Lists:onEndReached')
     try {
-      await list.loadMore()
+      await fetchNextPage()
     } catch (err) {
       logger.error('Failed to load more lists', {error: err})
     }
-  }, [list, track])
+  }, [isFetching, hasNextPage, isError, fetchNextPage, track])
 
   const onPressRetryLoadMore = React.useCallback(() => {
-    list.retryLoadMore()
-  }, [list])
+    fetchNextPage()
+  }, [fetchNextPage])
 
   const onPressEditMembership = React.useCallback(
     (profile: AppBskyActorDefs.ProfileViewBasic) => {
-      store.shell.openModal({
+      openModal({
         name: 'user-add-remove-lists',
         subject: profile.did,
         displayName: profile.displayName || profile.handle,
-        onAdd(listUri: string) {
-          if (listUri === list.uri) {
-            list.cacheAddMember(profile)
-          }
-        },
-        onRemove(listUri: string) {
-          if (listUri === list.uri) {
-            list.cacheRemoveMember(profile)
-          }
-        },
       })
     },
-    [store, list],
+    [openModal],
   )
 
   // rendering
@@ -139,7 +142,7 @@ export const ListItems = observer(function ListItemsImpl({
 
   const renderMemberButton = React.useCallback(
     (profile: AppBskyActorDefs.ProfileViewBasic) => {
-      if (!list.isOwner) {
+      if (!isOwner) {
         return null
       }
       return (
@@ -151,7 +154,7 @@ export const ListItems = observer(function ListItemsImpl({
         />
       )
     },
-    [list, onPressEditMembership],
+    [isOwner, onPressEditMembership],
   )
 
   const renderItem = React.useCallback(
@@ -161,7 +164,7 @@ export const ListItems = observer(function ListItemsImpl({
       } else if (item === ERROR_ITEM) {
         return (
           <ErrorMessage
-            message={list.error}
+            message={cleanError(error)}
             onPressTryAgain={onPressTryAgain}
           />
         )
@@ -189,7 +192,7 @@ export const ListItems = observer(function ListItemsImpl({
     [
       renderMemberButton,
       renderEmptyState,
-      list.error,
+      error,
       onPressTryAgain,
       onPressRetryLoadMore,
       isMobile,
@@ -199,19 +202,20 @@ export const ListItems = observer(function ListItemsImpl({
   const Footer = React.useCallback(
     () => (
       <View style={{paddingTop: 20, paddingBottom: 200}}>
-        {list.isLoading && <ActivityIndicator />}
+        {isFetching && <ActivityIndicator />}
       </View>
     ),
-    [list.isLoading],
+    [isFetching],
   )
 
+  const scrollHandler = useAnimatedScrollHandler(onScroll)
   return (
     <View testID={testID} style={style}>
       <FlatList
         testID={testID ? `${testID}-flatlist` : undefined}
         ref={scrollElRef}
-        data={data}
-        keyExtractor={(item: any) => item._reactKey}
+        data={items}
+        keyExtractor={(item: any) => item.subject?.did || item._reactKey}
         renderItem={renderItem}
         ListHeaderComponent={renderHeader}
         ListFooterComponent={Footer}
@@ -224,9 +228,11 @@ export const ListItems = observer(function ListItemsImpl({
             progressViewOffset={headerOffset}
           />
         }
-        contentContainerStyle={s.contentContainer}
+        contentContainerStyle={{
+          minHeight: Dimensions.get('window').height * 1.5,
+        }}
         style={{paddingTop: headerOffset}}
-        onScroll={onScroll}
+        onScroll={scrollHandler}
         onEndReached={onEndReached}
         onEndReachedThreshold={0.6}
         scrollEventThrottle={scrollEventThrottle}
@@ -237,4 +243,4 @@ export const ListItems = observer(function ListItemsImpl({
       />
     </View>
   )
-})
+}
diff --git a/src/view/com/lists/ListsList.tsx b/src/view/com/lists/MyLists.tsx
index 8c6510886..2c080582e 100644
--- a/src/view/com/lists/ListsList.tsx
+++ b/src/view/com/lists/MyLists.tsx
@@ -8,94 +8,71 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
-import {observer} from 'mobx-react-lite'
 import {AppBskyGraphDefs as GraphDefs} from '@atproto/api'
 import {ListCard} from './ListCard'
+import {MyListsFilter, useMyListsQuery} from '#/state/queries/my-lists'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
 import {Text} from '../util/text/Text'
-import {ListsListModel} from 'state/models/lists/lists-list'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {FlatList} from '../util/Views'
 import {s} from 'lib/styles'
 import {logger} from '#/logger'
+import {Trans} from '@lingui/macro'
+import {cleanError} from '#/lib/strings/errors'
 
 const LOADING = {_reactKey: '__loading__'}
 const EMPTY = {_reactKey: '__empty__'}
 const ERROR_ITEM = {_reactKey: '__error__'}
-const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
 
-export const ListsList = observer(function ListsListImpl({
-  listsList,
+export function MyLists({
+  filter,
   inline,
   style,
-  onPressTryAgain,
   renderItem,
   testID,
 }: {
-  listsList: ListsListModel
+  filter: MyListsFilter
   inline?: boolean
   style?: StyleProp<ViewStyle>
-  onPressTryAgain?: () => void
   renderItem?: (list: GraphDefs.ListView, index: number) => JSX.Element
   testID?: string
 }) {
   const pal = usePalette('default')
   const {track} = useAnalytics()
-  const [isRefreshing, setIsRefreshing] = React.useState(false)
+  const [isPTRing, setIsPTRing] = React.useState(false)
+  const {data, isFetching, isFetched, isError, error, refetch} =
+    useMyListsQuery(filter)
+  const isEmpty = !isFetching && !data?.length
 
-  const data = React.useMemo(() => {
+  const items = React.useMemo(() => {
     let items: any[] = []
-    if (listsList.hasError) {
+    if (isError && isEmpty) {
       items = items.concat([ERROR_ITEM])
     }
-    if (!listsList.hasLoaded && listsList.isLoading) {
+    if (!isFetched && isFetching) {
       items = items.concat([LOADING])
-    } else if (listsList.isEmpty) {
+    } else if (isEmpty) {
       items = items.concat([EMPTY])
     } else {
-      items = items.concat(listsList.lists)
-    }
-    if (listsList.loadMoreError) {
-      items = items.concat([LOAD_MORE_ERROR_ITEM])
+      items = items.concat(data)
     }
     return items
-  }, [
-    listsList.hasError,
-    listsList.hasLoaded,
-    listsList.isLoading,
-    listsList.lists,
-    listsList.isEmpty,
-    listsList.loadMoreError,
-  ])
+  }, [isError, isEmpty, isFetched, isFetching, data])
 
   // events
   // =
 
   const onRefresh = React.useCallback(async () => {
     track('Lists:onRefresh')
-    setIsRefreshing(true)
+    setIsPTRing(true)
     try {
-      await listsList.refresh()
+      await refetch()
     } catch (err) {
       logger.error('Failed to refresh lists', {error: err})
     }
-    setIsRefreshing(false)
-  }, [listsList, track, setIsRefreshing])
-
-  const onEndReached = React.useCallback(async () => {
-    track('Lists:onEndReached')
-    try {
-      await listsList.loadMore()
-    } catch (err) {
-      logger.error('Failed to load more lists', {error: err})
-    }
-  }, [listsList, track])
-
-  const onPressRetryLoadMore = React.useCallback(() => {
-    listsList.retryLoadMore()
-  }, [listsList])
+    setIsPTRing(false)
+  }, [refetch, track, setIsPTRing])
 
   // rendering
   // =
@@ -107,21 +84,16 @@ export const ListsList = observer(function ListsListImpl({
           <View
             testID="listsEmpty"
             style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
-            <Text style={pal.textLight}>You have no lists.</Text>
+            <Text style={pal.textLight}>
+              <Trans>You have no lists.</Trans>
+            </Text>
           </View>
         )
       } else if (item === ERROR_ITEM) {
         return (
           <ErrorMessage
-            message={listsList.error}
-            onPressTryAgain={onPressTryAgain}
-          />
-        )
-      } else if (item === LOAD_MORE_ERROR_ITEM) {
-        return (
-          <LoadMoreRetryBtn
-            label="There was an issue fetching your lists. Tap here to try again."
-            onPress={onPressRetryLoadMore}
+            message={cleanError(error)}
+            onPressTryAgain={onRefresh}
           />
         )
       } else if (item === LOADING) {
@@ -141,29 +113,27 @@ export const ListsList = observer(function ListsListImpl({
         />
       )
     },
-    [listsList, onPressTryAgain, onPressRetryLoadMore, renderItem, pal],
+    [error, onRefresh, renderItem, pal],
   )
 
   const FlatListCom = inline ? RNFlatList : FlatList
   return (
     <View testID={testID} style={style}>
-      {data.length > 0 && (
+      {items.length > 0 && (
         <FlatListCom
           testID={testID ? `${testID}-flatlist` : undefined}
-          data={data}
+          data={items}
           keyExtractor={(item: any) => item._reactKey}
           renderItem={renderItemInner}
           refreshControl={
             <RefreshControl
-              refreshing={isRefreshing}
+              refreshing={isPTRing}
               onRefresh={onRefresh}
               tintColor={pal.colors.text}
               titleColor={pal.colors.text}
             />
           }
           contentContainerStyle={[s.contentContainer]}
-          onEndReached={onEndReached}
-          onEndReachedThreshold={0.6}
           removeClippedSubviews={true}
           // @ts-ignore our .web version only -prf
           desktopFixedHeight
@@ -171,7 +141,7 @@ export const ListsList = observer(function ListsListImpl({
       )}
     </View>
   )
-})
+}
 
 const styles = StyleSheet.create({
   item: {
diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx
new file mode 100644
index 000000000..95cf8fde6
--- /dev/null
+++ b/src/view/com/lists/ProfileLists.tsx
@@ -0,0 +1,226 @@
+import React, {MutableRefObject} from 'react'
+import {
+  Dimensions,
+  RefreshControl,
+  StyleProp,
+  StyleSheet,
+  View,
+  ViewStyle,
+} from 'react-native'
+import {useQueryClient} from '@tanstack/react-query'
+import {FlatList} from '../util/Views'
+import {ListCard} from './ListCard'
+import {ErrorMessage} from '../util/error/ErrorMessage'
+import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
+import {Text} from '../util/text/Text'
+import {useAnalytics} from 'lib/analytics/analytics'
+import {usePalette} from 'lib/hooks/usePalette'
+import {useProfileListsQuery, RQKEY} from '#/state/queries/profile-lists'
+import {OnScrollHandler} from '#/lib/hooks/useOnMainScroll'
+import {logger} from '#/logger'
+import {Trans} from '@lingui/macro'
+import {cleanError} from '#/lib/strings/errors'
+import {useAnimatedScrollHandler} from 'react-native-reanimated'
+import {useTheme} from '#/lib/ThemeContext'
+import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
+
+const LOADING = {_reactKey: '__loading__'}
+const EMPTY = {_reactKey: '__empty__'}
+const ERROR_ITEM = {_reactKey: '__error__'}
+const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
+
+interface SectionRef {
+  scrollToTop: () => void
+}
+
+interface ProfileListsProps {
+  did: string
+  scrollElRef: MutableRefObject<FlatList<any> | null>
+  onScroll?: OnScrollHandler
+  scrollEventThrottle?: number
+  headerOffset: number
+  enabled?: boolean
+  style?: StyleProp<ViewStyle>
+  testID?: string
+}
+
+export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
+  function ProfileListsImpl(
+    {
+      did,
+      scrollElRef,
+      onScroll,
+      scrollEventThrottle,
+      headerOffset,
+      enabled,
+      style,
+      testID,
+    },
+    ref,
+  ) {
+    const pal = usePalette('default')
+    const theme = useTheme()
+    const {track} = useAnalytics()
+    const [isPTRing, setIsPTRing] = React.useState(false)
+    const opts = React.useMemo(() => ({enabled}), [enabled])
+    const {
+      data,
+      isFetching,
+      isFetched,
+      hasNextPage,
+      fetchNextPage,
+      isError,
+      error,
+      refetch,
+    } = useProfileListsQuery(did, opts)
+    const isEmpty = !isFetching && !data?.pages[0]?.lists.length
+
+    const items = React.useMemo(() => {
+      let items: any[] = []
+      if (isError && isEmpty) {
+        items = items.concat([ERROR_ITEM])
+      }
+      if (!isFetched && isFetching) {
+        items = items.concat([LOADING])
+      } else if (isEmpty) {
+        items = items.concat([EMPTY])
+      } else if (data?.pages) {
+        for (const page of data?.pages) {
+          items = items.concat(
+            page.lists.map(l => ({
+              ...l,
+              _reactKey: l.uri,
+            })),
+          )
+        }
+      }
+      if (isError && !isEmpty) {
+        items = items.concat([LOAD_MORE_ERROR_ITEM])
+      }
+      return items
+    }, [isError, isEmpty, isFetched, isFetching, data])
+
+    // events
+    // =
+
+    const queryClient = useQueryClient()
+
+    const onScrollToTop = React.useCallback(() => {
+      scrollElRef.current?.scrollToOffset({offset: -headerOffset})
+      queryClient.invalidateQueries({queryKey: RQKEY(did)})
+    }, [scrollElRef, queryClient, headerOffset, did])
+
+    React.useImperativeHandle(ref, () => ({
+      scrollToTop: onScrollToTop,
+    }))
+
+    const onRefresh = React.useCallback(async () => {
+      track('Lists:onRefresh')
+      setIsPTRing(true)
+      try {
+        await refetch()
+      } catch (err) {
+        logger.error('Failed to refresh lists', {error: err})
+      }
+      setIsPTRing(false)
+    }, [refetch, track, setIsPTRing])
+
+    const onEndReached = React.useCallback(async () => {
+      if (isFetching || !hasNextPage || isError) return
+
+      track('Lists:onEndReached')
+      try {
+        await fetchNextPage()
+      } catch (err) {
+        logger.error('Failed to load more lists', {error: err})
+      }
+    }, [isFetching, hasNextPage, isError, fetchNextPage, track])
+
+    const onPressRetryLoadMore = React.useCallback(() => {
+      fetchNextPage()
+    }, [fetchNextPage])
+
+    // rendering
+    // =
+
+    const renderItemInner = React.useCallback(
+      ({item}: {item: any}) => {
+        if (item === EMPTY) {
+          return (
+            <View
+              testID="listsEmpty"
+              style={[{padding: 18, borderTopWidth: 1}, pal.border]}>
+              <Text style={pal.textLight}>
+                <Trans>You have no lists.</Trans>
+              </Text>
+            </View>
+          )
+        } else if (item === ERROR_ITEM) {
+          return (
+            <ErrorMessage
+              message={cleanError(error)}
+              onPressTryAgain={refetch}
+            />
+          )
+        } else if (item === LOAD_MORE_ERROR_ITEM) {
+          return (
+            <LoadMoreRetryBtn
+              label="There was an issue fetching your lists. Tap here to try again."
+              onPress={onPressRetryLoadMore}
+            />
+          )
+        } else if (item === LOADING) {
+          return <FeedLoadingPlaceholder />
+        }
+        return (
+          <ListCard
+            list={item}
+            testID={`list-${item.name}`}
+            style={styles.item}
+          />
+        )
+      },
+      [error, refetch, onPressRetryLoadMore, pal],
+    )
+
+    const scrollHandler = useAnimatedScrollHandler(onScroll || {})
+    return (
+      <View testID={testID} style={style}>
+        <FlatList
+          testID={testID ? `${testID}-flatlist` : undefined}
+          ref={scrollElRef}
+          data={items}
+          keyExtractor={(item: any) => item._reactKey}
+          renderItem={renderItemInner}
+          refreshControl={
+            <RefreshControl
+              refreshing={isPTRing}
+              onRefresh={onRefresh}
+              tintColor={pal.colors.text}
+              titleColor={pal.colors.text}
+              progressViewOffset={headerOffset}
+            />
+          }
+          contentContainerStyle={{
+            minHeight: Dimensions.get('window').height * 1.5,
+          }}
+          style={{paddingTop: headerOffset}}
+          onScroll={onScroll != null ? scrollHandler : undefined}
+          scrollEventThrottle={scrollEventThrottle}
+          indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
+          removeClippedSubviews={true}
+          contentOffset={{x: 0, y: headerOffset * -1}}
+          // @ts-ignore our .web version only -prf
+          desktopFixedHeight
+          onEndReached={onEndReached}
+        />
+      </View>
+    )
+  },
+)
+
+const styles = StyleSheet.create({
+  item: {
+    paddingHorizontal: 18,
+  },
+})