about summary refs log tree commit diff
path: root/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/state')
-rw-r--r--src/state/cache/post-shadow.ts2
-rw-r--r--src/state/queries/pinned-post.ts87
-rw-r--r--src/state/queries/post-feed.ts1
-rw-r--r--src/state/queries/profile.ts3
-rw-r--r--src/state/queries/suggested-follows.ts11
5 files changed, 98 insertions, 6 deletions
diff --git a/src/state/cache/post-shadow.ts b/src/state/cache/post-shadow.ts
index 65300a8ef..b456a76d9 100644
--- a/src/state/cache/post-shadow.ts
+++ b/src/state/cache/post-shadow.ts
@@ -21,6 +21,7 @@ export interface PostShadow {
   repostUri: string | undefined
   isDeleted: boolean
   embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined
+  pinned: boolean
 }
 
 export const POST_TOMBSTONE = Symbol('PostTombstone')
@@ -113,6 +114,7 @@ function mergeShadow(
       ...(post.viewer || {}),
       like: 'likeUri' in shadow ? shadow.likeUri : post.viewer?.like,
       repost: 'repostUri' in shadow ? shadow.repostUri : post.viewer?.repost,
+      pinned: 'pinned' in shadow ? shadow.pinned : post.viewer?.pinned,
     },
   })
 }
diff --git a/src/state/queries/pinned-post.ts b/src/state/queries/pinned-post.ts
new file mode 100644
index 000000000..7e2c8ee79
--- /dev/null
+++ b/src/state/queries/pinned-post.ts
@@ -0,0 +1,87 @@
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {useMutation, useQueryClient} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
+import * as Toast from '#/view/com/util/Toast'
+import {updatePostShadow} from '../cache/post-shadow'
+import {useAgent, useSession} from '../session'
+import {useProfileUpdateMutation} from './profile'
+
+export function usePinnedPostMutation() {
+  const {_} = useLingui()
+  const {currentAccount} = useSession()
+  const agent = useAgent()
+  const queryClient = useQueryClient()
+  const {mutateAsync: profileUpdateMutate} = useProfileUpdateMutation()
+
+  return useMutation({
+    mutationFn: async ({
+      postUri,
+      postCid,
+      action,
+    }: {
+      postUri: string
+      postCid: string
+      action: 'pin' | 'unpin'
+    }) => {
+      const pinCurrentPost = action === 'pin'
+      let prevPinnedPost: string | undefined
+      try {
+        updatePostShadow(queryClient, postUri, {pinned: pinCurrentPost})
+
+        // get the currently pinned post so we can optimistically remove the pin from it
+        if (!currentAccount) throw new Error('Not logged in')
+        const {data: profile} = await agent.getProfile({
+          actor: currentAccount.did,
+        })
+        prevPinnedPost = profile.pinnedPost?.uri
+        if (prevPinnedPost && prevPinnedPost !== postUri) {
+          updatePostShadow(queryClient, prevPinnedPost, {pinned: false})
+        }
+
+        await profileUpdateMutate({
+          profile,
+          updates: existing => {
+            existing.pinnedPost = pinCurrentPost
+              ? {uri: postUri, cid: postCid}
+              : undefined
+            return existing
+          },
+          checkCommitted: res =>
+            pinCurrentPost
+              ? res.data.pinnedPost?.uri === postUri
+              : !res.data.pinnedPost,
+        })
+
+        if (pinCurrentPost) {
+          Toast.show(_(msg`Post pinned`))
+        } else {
+          Toast.show(_(msg`Post unpinned`))
+        }
+
+        queryClient.invalidateQueries({
+          queryKey: FEED_RQKEY(
+            `author|${currentAccount.did}|posts_and_author_threads`,
+          ),
+        })
+        queryClient.invalidateQueries({
+          queryKey: FEED_RQKEY(
+            `author|${currentAccount.did}|posts_with_replies`,
+          ),
+        })
+      } catch (e: any) {
+        Toast.show(_(msg`Failed to pin post`))
+        logger.error('Failed to pin post', {message: String(e)})
+        // revert optimistic update
+        updatePostShadow(queryClient, postUri, {
+          pinned: !pinCurrentPost,
+        })
+        if (prevPinnedPost && prevPinnedPost !== postUri) {
+          updatePostShadow(queryClient, prevPinnedPost, {pinned: true})
+        }
+      }
+    },
+  })
+}
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index 07c5da81b..1785eb445 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -91,6 +91,7 @@ export interface FeedPostSlice {
   feedContext: string | undefined
   reason?:
     | AppBskyFeedDefs.ReasonRepost
+    | AppBskyFeedDefs.ReasonPin
     | ReasonFeedSource
     | {[k: string]: unknown; $type: string}
 }
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 78a142eea..3059d9efe 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -159,6 +159,9 @@ export function useProfileUpdateMutation() {
         } else {
           existing.displayName = updates.displayName
           existing.description = updates.description
+          if ('pinnedPost' in updates) {
+            existing.pinnedPost = updates.pinnedPost
+          }
         }
         if (newUserAvatarPromise) {
           const res = await newUserAvatarPromise
diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts
index 5ae831704..07e16946e 100644
--- a/src/state/queries/suggested-follows.ts
+++ b/src/state/queries/suggested-follows.ts
@@ -105,17 +105,16 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) {
 
 export function useSuggestedFollowsByActorQuery({did}: {did: string}) {
   const agent = useAgent()
-  return useQuery<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({
+  return useQuery({
     queryKey: suggestedFollowsByActorQueryKey(did),
     queryFn: async () => {
       const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({
         actor: did,
       })
-      const data = res.data.isFallback ? {suggestions: []} : res.data
-      data.suggestions = data.suggestions.filter(profile => {
-        return !profile.viewer?.following
-      })
-      return data
+      const suggestions = res.data.isFallback
+        ? []
+        : res.data.suggestions.filter(profile => !profile.viewer?.following)
+      return {suggestions}
     },
   })
 }