about summary refs log tree commit diff
path: root/src/state/queries
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/queries')
-rw-r--r--src/state/queries/post-feed.ts176
-rw-r--r--src/state/queries/post-thread.ts10
-rw-r--r--src/state/queries/post.ts98
-rw-r--r--src/state/queries/resolve-uri.ts17
4 files changed, 237 insertions, 64 deletions
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
new file mode 100644
index 000000000..1a391d5c3
--- /dev/null
+++ b/src/state/queries/post-feed.ts
@@ -0,0 +1,176 @@
+import {useCallback, useMemo} from 'react'
+import {AppBskyFeedDefs, AppBskyFeedPost, moderatePost} from '@atproto/api'
+import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
+import {useSession} from '../session'
+import {useFeedTuners} from '../preferences/feed-tuners'
+import {FeedTuner, NoopFeedTuner} from 'lib/api/feed-manip'
+import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
+import {FollowingFeedAPI} from 'lib/api/feed/following'
+import {AuthorFeedAPI} from 'lib/api/feed/author'
+import {LikesFeedAPI} from 'lib/api/feed/likes'
+import {CustomFeedAPI} from 'lib/api/feed/custom'
+import {ListFeedAPI} from 'lib/api/feed/list'
+import {MergeFeedAPI} from 'lib/api/feed/merge'
+import {useStores} from '../models/root-store'
+
+type ActorDid = string
+type AuthorFilter =
+  | 'posts_with_replies'
+  | 'posts_no_replies'
+  | 'posts_with_media'
+type FeedUri = string
+type ListUri = string
+export type FeedDescriptor =
+  | 'home'
+  | 'following'
+  | `author|${ActorDid}|${AuthorFilter}`
+  | `feedgen|${FeedUri}`
+  | `likes|${ActorDid}`
+  | `list|${ListUri}`
+export interface FeedParams {
+  disableTuner?: boolean
+  mergeFeedEnabled?: boolean
+  mergeFeedSources?: string[]
+}
+
+type RQPageParam = string | undefined
+
+export function RQKEY(feedDesc: FeedDescriptor, params?: FeedParams) {
+  return ['post-feed', feedDesc, params || {}]
+}
+
+export interface FeedPostSliceItem {
+  _reactKey: string
+  uri: string
+  post: AppBskyFeedDefs.PostView
+  record: AppBskyFeedPost.Record
+  reason?: AppBskyFeedDefs.ReasonRepost | ReasonFeedSource
+}
+
+export interface FeedPostSlice {
+  _reactKey: string
+  rootUri: string
+  isThread: boolean
+  items: FeedPostSliceItem[]
+}
+
+export interface FeedPage {
+  cursor: string | undefined
+  slices: FeedPostSlice[]
+}
+
+export function usePostFeedQuery(
+  feedDesc: FeedDescriptor,
+  params?: FeedParams,
+  opts?: {enabled?: boolean},
+) {
+  const {agent} = useSession()
+  const feedTuners = useFeedTuners(feedDesc)
+  const store = useStores()
+  const enabled = opts?.enabled !== false
+
+  const api: FeedAPI = useMemo(() => {
+    if (feedDesc === 'home') {
+      return new MergeFeedAPI(agent, params || {}, feedTuners)
+    } else if (feedDesc === 'following') {
+      return new FollowingFeedAPI(agent)
+    } else if (feedDesc.startsWith('author')) {
+      const [_, actor, filter] = feedDesc.split('|')
+      return new AuthorFeedAPI(agent, {actor, filter})
+    } else if (feedDesc.startsWith('likes')) {
+      const [_, actor] = feedDesc.split('|')
+      return new LikesFeedAPI(agent, {actor})
+    } else if (feedDesc.startsWith('feedgen')) {
+      const [_, feed] = feedDesc.split('|')
+      return new CustomFeedAPI(agent, {feed})
+    } else if (feedDesc.startsWith('list')) {
+      const [_, list] = feedDesc.split('|')
+      return new ListFeedAPI(agent, {list})
+    } else {
+      // shouldnt happen
+      return new FollowingFeedAPI(agent)
+    }
+  }, [feedDesc, params, feedTuners, agent])
+  const tuner = useMemo(
+    () => (params?.disableTuner ? new NoopFeedTuner() : new FeedTuner()),
+    [params],
+  )
+
+  const pollLatest = useCallback(async () => {
+    if (!enabled) {
+      return false
+    }
+    console.log('poll')
+    const post = await api.peekLatest()
+    if (post) {
+      const slices = tuner.tune([post], feedTuners, {
+        dryRun: true,
+        maintainOrder: true,
+      })
+      if (slices[0]) {
+        if (
+          !moderatePost(
+            slices[0].items[0].post,
+            store.preferences.moderationOpts,
+          ).content.filter
+        ) {
+          return true
+        }
+      }
+    }
+    return false
+  }, [api, tuner, feedTuners, store.preferences.moderationOpts, enabled])
+
+  const out = useInfiniteQuery<
+    FeedPage,
+    Error,
+    InfiniteData<FeedPage>,
+    QueryKey,
+    RQPageParam
+  >({
+    queryKey: RQKEY(feedDesc, params),
+    async queryFn({pageParam}: {pageParam: RQPageParam}) {
+      console.log('fetch', feedDesc, pageParam)
+      if (!pageParam) {
+        tuner.reset()
+      }
+      const res = await api.fetch({cursor: pageParam, limit: 30})
+      const slices = tuner.tune(res.feed, feedTuners)
+      return {
+        cursor: res.cursor,
+        slices: slices.map(slice => ({
+          _reactKey: slice._reactKey,
+          rootUri: slice.rootItem.post.uri,
+          isThread:
+            slice.items.length > 1 &&
+            slice.items.every(
+              item => item.post.author.did === slice.items[0].post.author.did,
+            ),
+          source: undefined, // TODO
+          items: slice.items
+            .map((item, i) => {
+              if (
+                AppBskyFeedPost.isRecord(item.post.record) &&
+                AppBskyFeedPost.validateRecord(item.post.record).success
+              ) {
+                return {
+                  _reactKey: `${slice._reactKey}-${i}`,
+                  uri: item.post.uri,
+                  post: item.post,
+                  record: item.post.record,
+                  reason: i === 0 && slice.source ? slice.source : item.reason,
+                }
+              }
+              return undefined
+            })
+            .filter(Boolean) as FeedPostSliceItem[],
+        })),
+      }
+    },
+    initialPageParam: undefined,
+    getNextPageParam: lastPage => lastPage.cursor,
+    enabled,
+  })
+
+  return {...out, pollLatest}
+}
diff --git a/src/state/queries/post-thread.ts b/src/state/queries/post-thread.ts
index 4dea8aaf1..386c70483 100644
--- a/src/state/queries/post-thread.ts
+++ b/src/state/queries/post-thread.ts
@@ -57,17 +57,17 @@ export type ThreadNode =
 
 export function usePostThreadQuery(uri: string | undefined) {
   const {agent} = useSession()
-  return useQuery<ThreadNode, Error>(
-    RQKEY(uri || ''),
-    async () => {
+  return useQuery<ThreadNode, Error>({
+    queryKey: RQKEY(uri || ''),
+    async queryFn() {
       const res = await agent.getPostThread({uri: uri!})
       if (res.success) {
         return responseToThreadNodes(res.data.thread)
       }
       return {type: 'unknown', uri: uri!}
     },
-    {enabled: !!uri},
-  )
+    enabled: !!uri,
+  })
 }
 
 export function sortThread(
diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts
index f62190c67..ffff7f967 100644
--- a/src/state/queries/post.ts
+++ b/src/state/queries/post.ts
@@ -7,9 +7,9 @@ export const RQKEY = (postUri: string) => ['post', postUri]
 
 export function usePostQuery(uri: string | undefined) {
   const {agent} = useSession()
-  return useQuery<AppBskyFeedDefs.PostView>(
-    RQKEY(uri || ''),
-    async () => {
+  return useQuery<AppBskyFeedDefs.PostView>({
+    queryKey: RQKEY(uri || ''),
+    async queryFn() {
       const res = await agent.getPosts({uris: [uri!]})
       if (res.success && res.data.posts[0]) {
         return res.data.posts[0]
@@ -17,10 +17,8 @@ export function usePostQuery(uri: string | undefined) {
 
       throw new Error('No data')
     },
-    {
-      enabled: !!uri,
-    },
-  )
+    enabled: !!uri,
+  })
 }
 
 export function usePostLikeMutation() {
@@ -29,7 +27,8 @@ export function usePostLikeMutation() {
     {uri: string}, // responds with the uri of the like
     Error,
     {uri: string; cid: string; likeCount: number} // the post's uri, cid, and likes
-  >(post => agent.like(post.uri, post.cid), {
+  >({
+    mutationFn: post => agent.like(post.uri, post.cid),
     onMutate(variables) {
       // optimistically update the post-shadow
       updatePostShadow(variables.uri, {
@@ -59,27 +58,25 @@ export function usePostUnlikeMutation() {
     void,
     Error,
     {postUri: string; likeUri: string; likeCount: number}
-  >(
-    async ({likeUri}) => {
+  >({
+    mutationFn: async ({likeUri}) => {
       await agent.deleteLike(likeUri)
     },
-    {
-      onMutate(variables) {
-        // optimistically update the post-shadow
-        updatePostShadow(variables.postUri, {
-          likeCount: variables.likeCount - 1,
-          likeUri: undefined,
-        })
-      },
-      onError(error, variables) {
-        // revert the optimistic update
-        updatePostShadow(variables.postUri, {
-          likeCount: variables.likeCount,
-          likeUri: variables.likeUri,
-        })
-      },
+    onMutate(variables) {
+      // optimistically update the post-shadow
+      updatePostShadow(variables.postUri, {
+        likeCount: variables.likeCount - 1,
+        likeUri: undefined,
+      })
+    },
+    onError(error, variables) {
+      // revert the optimistic update
+      updatePostShadow(variables.postUri, {
+        likeCount: variables.likeCount,
+        likeUri: variables.likeUri,
+      })
     },
-  )
+  })
 }
 
 export function usePostRepostMutation() {
@@ -88,7 +85,8 @@ export function usePostRepostMutation() {
     {uri: string}, // responds with the uri of the repost
     Error,
     {uri: string; cid: string; repostCount: number} // the post's uri, cid, and reposts
-  >(post => agent.repost(post.uri, post.cid), {
+  >({
+    mutationFn: post => agent.repost(post.uri, post.cid),
     onMutate(variables) {
       // optimistically update the post-shadow
       updatePostShadow(variables.uri, {
@@ -118,39 +116,35 @@ export function usePostUnrepostMutation() {
     void,
     Error,
     {postUri: string; repostUri: string; repostCount: number}
-  >(
-    async ({repostUri}) => {
+  >({
+    mutationFn: async ({repostUri}) => {
       await agent.deleteRepost(repostUri)
     },
-    {
-      onMutate(variables) {
-        // optimistically update the post-shadow
-        updatePostShadow(variables.postUri, {
-          repostCount: variables.repostCount - 1,
-          repostUri: undefined,
-        })
-      },
-      onError(error, variables) {
-        // revert the optimistic update
-        updatePostShadow(variables.postUri, {
-          repostCount: variables.repostCount,
-          repostUri: variables.repostUri,
-        })
-      },
+    onMutate(variables) {
+      // optimistically update the post-shadow
+      updatePostShadow(variables.postUri, {
+        repostCount: variables.repostCount - 1,
+        repostUri: undefined,
+      })
+    },
+    onError(error, variables) {
+      // revert the optimistic update
+      updatePostShadow(variables.postUri, {
+        repostCount: variables.repostCount,
+        repostUri: variables.repostUri,
+      })
     },
-  )
+  })
 }
 
 export function usePostDeleteMutation() {
   const {agent} = useSession()
-  return useMutation<void, Error, {uri: string}>(
-    async ({uri}) => {
+  return useMutation<void, Error, {uri: string}>({
+    mutationFn: async ({uri}) => {
       await agent.deletePost(uri)
     },
-    {
-      onSuccess(data, variables) {
-        updatePostShadow(variables.uri, {isDeleted: true})
-      },
+    onSuccess(data, variables) {
+      updatePostShadow(variables.uri, {isDeleted: true})
     },
-  )
+  })
 }
diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts
index 770be5cf8..26e0a475b 100644
--- a/src/state/queries/resolve-uri.ts
+++ b/src/state/queries/resolve-uri.ts
@@ -6,12 +6,15 @@ export const RQKEY = (uri: string) => ['resolved-uri', uri]
 
 export function useResolveUriQuery(uri: string) {
   const {agent} = useSession()
-  return useQuery<string | undefined, Error>(RQKEY(uri), async () => {
-    const urip = new AtUri(uri)
-    if (!urip.host.startsWith('did:')) {
-      const res = await agent.resolveHandle({handle: urip.host})
-      urip.host = res.data.did
-    }
-    return urip.toString()
+  return useQuery<string | undefined, Error>({
+    queryKey: RQKEY(uri),
+    async queryFn() {
+      const urip = new AtUri(uri)
+      if (!urip.host.startsWith('did:')) {
+        const res = await agent.resolveHandle({handle: urip.host})
+        urip.host = res.data.did
+      }
+      return urip.toString()
+    },
   })
 }