about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authordan <dan.abramov@gmail.com>2023-11-15 22:04:25 +0000
committerGitHub <noreply@github.com>2023-11-15 14:04:25 -0800
commit839e8e8d0ade22ce47678229a98fe602c31601c3 (patch)
treeecac6c465d8d016182a709da6347d321968a1c95 /src
parente699df21c66f2f55d34af4d2a14c03d02274b43e (diff)
downloadvoidsky-839e8e8d0ade22ce47678229a98fe602c31601c3.tar.zst
Post PostLikedBy and PostRepostedBy to RQ (#1913)
* Port PostRepostedBy to RQ

* Port PostLikedBy to RQ
Diffstat (limited to 'src')
-rw-r--r--src/state/models/lists/likes.ts135
-rw-r--r--src/state/models/lists/reposted-by.ts136
-rw-r--r--src/state/queries/post-liked-by.ts32
-rw-r--r--src/state/queries/post-reposted-by.ts32
-rw-r--r--src/view/com/post-thread/PostLikedBy.tsx104
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx107
6 files changed, 202 insertions, 344 deletions
diff --git a/src/state/models/lists/likes.ts b/src/state/models/lists/likes.ts
deleted file mode 100644
index df20f09db..000000000
--- a/src/state/models/lists/likes.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {AtUri} from '@atproto/api'
-import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {cleanError} from 'lib/strings/errors'
-import {bundleAsync} from 'lib/async/bundle'
-import * as apilib from 'lib/api/index'
-import {logger} from '#/logger'
-
-const PAGE_SIZE = 30
-
-export type LikeItem = GetLikes.Like
-
-export class LikesModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  resolvedUri = ''
-  params: GetLikes.QueryParams
-  hasMore = true
-  loadMoreCursor?: string
-
-  // data
-  uri: string = ''
-  likes: LikeItem[] = []
-
-  constructor(public rootStore: RootStoreModel, params: GetLikes.QueryParams) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-        params: false,
-      },
-      {autoBind: true},
-    )
-    this.params = params
-  }
-
-  get hasContent() {
-    return this.uri !== ''
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (!replace && !this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      if (!this.resolvedUri) {
-        await this._resolveUri()
-      }
-      const params = Object.assign({}, this.params, {
-        uri: this.resolvedUri,
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      const res = await this.rootStore.agent.getLikes(params)
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
-      }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      logger.error('Failed to fetch likes', {error: err})
-    }
-  }
-
-  // helper functions
-  // =
-
-  async _resolveUri() {
-    const urip = new AtUri(this.params.uri)
-    if (!urip.host.startsWith('did:')) {
-      try {
-        urip.host = await apilib.resolveName(this.rootStore, urip.host)
-      } catch (e: any) {
-        this.error = e.toString()
-      }
-    }
-    runInAction(() => {
-      this.resolvedUri = urip.toString()
-    })
-  }
-
-  _replaceAll(res: GetLikes.Response) {
-    this.likes = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetLikes.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    this.rootStore.me.follows.hydrateMany(
-      res.data.likes.map(like => like.actor),
-    )
-    this.likes = this.likes.concat(res.data.likes)
-  }
-}
diff --git a/src/state/models/lists/reposted-by.ts b/src/state/models/lists/reposted-by.ts
deleted file mode 100644
index c5058558a..000000000
--- a/src/state/models/lists/reposted-by.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-import {makeAutoObservable, runInAction} from 'mobx'
-import {AtUri} from '@atproto/api'
-import {
-  AppBskyFeedGetRepostedBy as GetRepostedBy,
-  AppBskyActorDefs,
-} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {bundleAsync} from 'lib/async/bundle'
-import {cleanError} from 'lib/strings/errors'
-import * as apilib from 'lib/api/index'
-import {logger} from '#/logger'
-
-const PAGE_SIZE = 30
-
-export type RepostedByItem = AppBskyActorDefs.ProfileViewBasic
-
-export class RepostedByModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  resolvedUri = ''
-  params: GetRepostedBy.QueryParams
-  hasMore = true
-  loadMoreCursor?: string
-
-  // data
-  uri: string = ''
-  repostedBy: RepostedByItem[] = []
-
-  constructor(
-    public rootStore: RootStoreModel,
-    params: GetRepostedBy.QueryParams,
-  ) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-        params: false,
-      },
-      {autoBind: true},
-    )
-    this.params = params
-  }
-
-  get hasContent() {
-    return this.uri !== ''
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    this._xLoading(replace)
-    try {
-      if (!this.resolvedUri) {
-        await this._resolveUri()
-      }
-      const params = Object.assign({}, this.params, {
-        uri: this.resolvedUri,
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      const res = await this.rootStore.agent.getRepostedBy(params)
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
-      }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      logger.error('Failed to fetch reposted by view', {error: err})
-    }
-  }
-
-  // helper functions
-  // =
-
-  async _resolveUri() {
-    const urip = new AtUri(this.params.uri)
-    if (!urip.host.startsWith('did:')) {
-      try {
-        urip.host = await apilib.resolveName(this.rootStore, urip.host)
-      } catch (e: any) {
-        this.error = e.toString()
-      }
-    }
-    runInAction(() => {
-      this.resolvedUri = urip.toString()
-    })
-  }
-
-  _replaceAll(res: GetRepostedBy.Response) {
-    this.repostedBy = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetRepostedBy.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    this.repostedBy = this.repostedBy.concat(res.data.repostedBy)
-    this.rootStore.me.follows.hydrateMany(res.data.repostedBy)
-  }
-}
diff --git a/src/state/queries/post-liked-by.ts b/src/state/queries/post-liked-by.ts
new file mode 100644
index 000000000..78ce9f60a
--- /dev/null
+++ b/src/state/queries/post-liked-by.ts
@@ -0,0 +1,32 @@
+import {AppBskyFeedGetLikes} from '@atproto/api'
+import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
+import {useSession} from '../session'
+
+const PAGE_SIZE = 30
+type RQPageParam = string | undefined
+
+export const RQKEY = (resolvedUri: string) => ['post-liked-by', resolvedUri]
+
+export function usePostLikedByQuery(resolvedUri: string | undefined) {
+  const {agent} = useSession()
+  return useInfiniteQuery<
+    AppBskyFeedGetLikes.OutputSchema,
+    Error,
+    InfiniteData<AppBskyFeedGetLikes.OutputSchema>,
+    QueryKey,
+    RQPageParam
+  >({
+    queryKey: RQKEY(resolvedUri || ''),
+    async queryFn({pageParam}: {pageParam: RQPageParam}) {
+      const res = await agent.getLikes({
+        uri: resolvedUri || '',
+        limit: PAGE_SIZE,
+        cursor: pageParam,
+      })
+      return res.data
+    },
+    initialPageParam: undefined,
+    getNextPageParam: lastPage => lastPage.cursor,
+    enabled: !!resolvedUri,
+  })
+}
diff --git a/src/state/queries/post-reposted-by.ts b/src/state/queries/post-reposted-by.ts
new file mode 100644
index 000000000..15cb377b4
--- /dev/null
+++ b/src/state/queries/post-reposted-by.ts
@@ -0,0 +1,32 @@
+import {AppBskyFeedGetRepostedBy} from '@atproto/api'
+import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
+import {useSession} from '../session'
+
+const PAGE_SIZE = 30
+type RQPageParam = string | undefined
+
+export const RQKEY = (resolvedUri: string) => ['post-reposted-by', resolvedUri]
+
+export function usePostRepostedByQuery(resolvedUri: string | undefined) {
+  const {agent} = useSession()
+  return useInfiniteQuery<
+    AppBskyFeedGetRepostedBy.OutputSchema,
+    Error,
+    InfiniteData<AppBskyFeedGetRepostedBy.OutputSchema>,
+    QueryKey,
+    RQPageParam
+  >({
+    queryKey: RQKEY(resolvedUri || ''),
+    async queryFn({pageParam}: {pageParam: RQPageParam}) {
+      const res = await agent.getRepostedBy({
+        uri: resolvedUri || '',
+        limit: PAGE_SIZE,
+        cursor: pageParam,
+      })
+      return res.data
+    },
+    initialPageParam: undefined,
+    getNextPageParam: lastPage => lastPage.cursor,
+    enabled: !!resolvedUri,
+  })
+}
diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx
index 22ff035d0..d3b5ae47b 100644
--- a/src/view/com/post-thread/PostLikedBy.tsx
+++ b/src/view/com/post-thread/PostLikedBy.tsx
@@ -1,39 +1,74 @@
-import React, {useEffect} from 'react'
-import {observer} from 'mobx-react-lite'
+import React, {useCallback, useMemo, useState} from 'react'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
+import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
 import {CenteredView, FlatList} from '../util/Views'
-import {LikesModel, LikeItem} from 'state/models/lists/likes'
 import {ErrorMessage} from '../util/error/ErrorMessage'
 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {logger} from '#/logger'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {usePostLikedByQuery} from '#/state/queries/post-liked-by'
+import {cleanError} from '#/lib/strings/errors'
 
-export const PostLikedBy = observer(function PostLikedByImpl({
-  uri,
-}: {
-  uri: string
-}) {
+export function PostLikedBy({uri}: {uri: string}) {
   const pal = usePalette('default')
-  const store = useStores()
-  const view = React.useMemo(() => new LikesModel(store, {uri}), [store, uri])
+  const [isPTRing, setIsPTRing] = useState(false)
+  const {
+    data: resolvedUri,
+    error: resolveError,
+    isFetching: isFetchingResolvedUri,
+  } = useResolveUriQuery(uri)
+  const {
+    data,
+    dataUpdatedAt,
+    isFetching,
+    isFetched,
+    isFetchingNextPage,
+    hasNextPage,
+    fetchNextPage,
+    isError,
+    error,
+    refetch,
+  } = usePostLikedByQuery(resolvedUri?.uri)
+  const likes = useMemo(() => {
+    if (data?.pages) {
+      return data.pages.flatMap(page => page.likes)
+    }
+  }, [data])
 
-  useEffect(() => {
-    view
-      .loadMore()
-      .catch(err => logger.error('Failed to fetch likes', {error: err}))
-  }, [view])
+  const onRefresh = useCallback(async () => {
+    setIsPTRing(true)
+    try {
+      await refetch()
+    } catch (err) {
+      logger.error('Failed to refresh likes', {error: err})
+    }
+    setIsPTRing(false)
+  }, [refetch, setIsPTRing])
 
-  const onRefresh = () => {
-    view.refresh()
-  }
-  const onEndReached = () => {
-    view
-      .loadMore()
-      .catch(err => logger.error('Failed to load more likes', {error: err}))
-  }
+  const onEndReached = useCallback(async () => {
+    if (isFetching || !hasNextPage || isError) return
+    try {
+      await fetchNextPage()
+    } catch (err) {
+      logger.error('Failed to load more likes', {error: err})
+    }
+  }, [isFetching, hasNextPage, isError, fetchNextPage])
+
+  const renderItem = useCallback(
+    ({item}: {item: GetLikes.Like}) => {
+      return (
+        <ProfileCardWithFollowBtn
+          key={item.actor.did}
+          profile={item.actor}
+          dataUpdatedAt={dataUpdatedAt}
+        />
+      )
+    },
+    [dataUpdatedAt],
+  )
 
-  if (!view.hasLoaded) {
+  if (isFetchingResolvedUri || !isFetched) {
     return (
       <CenteredView>
         <ActivityIndicator />
@@ -43,26 +78,26 @@ export const PostLikedBy = observer(function PostLikedByImpl({
 
   // error
   // =
-  if (view.hasError) {
+  if (resolveError || isError) {
     return (
       <CenteredView>
-        <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
+        <ErrorMessage
+          message={cleanError(resolveError || error)}
+          onPressTryAgain={onRefresh}
+        />
       </CenteredView>
     )
   }
 
   // loaded
   // =
-  const renderItem = ({item}: {item: LikeItem}) => (
-    <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
-  )
   return (
     <FlatList
-      data={view.likes}
+      data={likes}
       keyExtractor={item => item.actor.did}
       refreshControl={
         <RefreshControl
-          refreshing={view.isRefreshing}
+          refreshing={isPTRing}
           onRefresh={onRefresh}
           tintColor={pal.colors.text}
           titleColor={pal.colors.text}
@@ -75,15 +110,14 @@ export const PostLikedBy = observer(function PostLikedByImpl({
       // eslint-disable-next-line react/no-unstable-nested-components
       ListFooterComponent={() => (
         <View style={styles.footer}>
-          {view.isLoading && <ActivityIndicator />}
+          {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
         </View>
       )}
-      extraData={view.isLoading}
       // @ts-ignore our .web version only -prf
       desktopFixedHeight
     />
   )
-})
+}
 
 const styles = StyleSheet.create({
   footer: {
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 29a795302..67c043a21 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -1,42 +1,74 @@
-import React, {useEffect} from 'react'
-import {observer} from 'mobx-react-lite'
+import React, {useMemo, useCallback, useState} from 'react'
 import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
+import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
 import {CenteredView, FlatList} from '../util/Views'
-import {RepostedByModel, RepostedByItem} from 'state/models/lists/reposted-by'
 import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
 import {ErrorMessage} from '../util/error/ErrorMessage'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {logger} from '#/logger'
+import {useResolveUriQuery} from '#/state/queries/resolve-uri'
+import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by'
+import {cleanError} from '#/lib/strings/errors'
 
-export const PostRepostedBy = observer(function PostRepostedByImpl({
-  uri,
-}: {
-  uri: string
-}) {
+export function PostRepostedBy({uri}: {uri: string}) {
   const pal = usePalette('default')
-  const store = useStores()
-  const view = React.useMemo(
-    () => new RepostedByModel(store, {uri}),
-    [store, uri],
-  )
+  const [isPTRing, setIsPTRing] = useState(false)
+  const {
+    data: resolvedUri,
+    error: resolveError,
+    isFetching: isFetchingResolvedUri,
+  } = useResolveUriQuery(uri)
+  const {
+    data,
+    dataUpdatedAt,
+    isFetching,
+    isFetched,
+    isFetchingNextPage,
+    hasNextPage,
+    fetchNextPage,
+    isError,
+    error,
+    refetch,
+  } = usePostRepostedByQuery(resolvedUri?.uri)
+  const repostedBy = useMemo(() => {
+    if (data?.pages) {
+      return data.pages.flatMap(page => page.repostedBy)
+    }
+  }, [data])
 
-  useEffect(() => {
-    view
-      .loadMore()
-      .catch(err => logger.error('Failed to fetch reposts', {error: err}))
-  }, [view])
+  const onRefresh = useCallback(async () => {
+    setIsPTRing(true)
+    try {
+      await refetch()
+    } catch (err) {
+      logger.error('Failed to refresh reposts', {error: err})
+    }
+    setIsPTRing(false)
+  }, [refetch, setIsPTRing])
 
-  const onRefresh = () => {
-    view.refresh()
-  }
-  const onEndReached = () => {
-    view
-      .loadMore()
-      .catch(err => logger.error('Failed to load more reposts', {error: err}))
-  }
+  const onEndReached = useCallback(async () => {
+    if (isFetching || !hasNextPage || isError) return
+    try {
+      await fetchNextPage()
+    } catch (err) {
+      logger.error('Failed to load more reposts', {error: err})
+    }
+  }, [isFetching, hasNextPage, isError, fetchNextPage])
 
-  if (!view.hasLoaded) {
+  const renderItem = useCallback(
+    ({item}: {item: ActorDefs.ProfileViewBasic}) => {
+      return (
+        <ProfileCardWithFollowBtn
+          key={item.did}
+          profile={item}
+          dataUpdatedAt={dataUpdatedAt}
+        />
+      )
+    },
+    [dataUpdatedAt],
+  )
+
+  if (isFetchingResolvedUri || !isFetched) {
     return (
       <CenteredView>
         <ActivityIndicator />
@@ -46,26 +78,26 @@ export const PostRepostedBy = observer(function PostRepostedByImpl({
 
   // error
   // =
-  if (view.hasError) {
+  if (resolveError || isError) {
     return (
       <CenteredView>
-        <ErrorMessage message={view.error} onPressTryAgain={onRefresh} />
+        <ErrorMessage
+          message={cleanError(resolveError || error)}
+          onPressTryAgain={onRefresh}
+        />
       </CenteredView>
     )
   }
 
   // loaded
   // =
-  const renderItem = ({item}: {item: RepostedByItem}) => (
-    <ProfileCardWithFollowBtn key={item.did} profile={item} />
-  )
   return (
     <FlatList
-      data={view.repostedBy}
+      data={repostedBy}
       keyExtractor={item => item.did}
       refreshControl={
         <RefreshControl
-          refreshing={view.isRefreshing}
+          refreshing={isPTRing}
           onRefresh={onRefresh}
           tintColor={pal.colors.text}
           titleColor={pal.colors.text}
@@ -78,15 +110,14 @@ export const PostRepostedBy = observer(function PostRepostedByImpl({
       // eslint-disable-next-line react/no-unstable-nested-components
       ListFooterComponent={() => (
         <View style={styles.footer}>
-          {view.isLoading && <ActivityIndicator />}
+          {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
         </View>
       )}
-      extraData={view.isLoading}
       // @ts-ignore our .web version only -prf
       desktopFixedHeight
     />
   )
-})
+}
 
 const styles = StyleSheet.create({
   footer: {