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/modals/index.tsx3
-rw-r--r--src/state/models/ui/shell.ts5
-rw-r--r--src/state/queries/profile-extra-info.ts31
-rw-r--r--src/state/queries/profile.ts99
4 files changed, 130 insertions, 8 deletions
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 6c63d9fc1..57f486630 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -3,7 +3,6 @@ import {AppBskyActorDefs, AppBskyGraphDefs, ModerationUI} from '@atproto/api'
 import {StyleProp, ViewStyle, DeviceEventEmitter} from 'react-native'
 import {Image as RNImage} from 'react-native-image-crop-picker'
 
-import {ProfileModel} from '#/state/models/content/profile'
 import {ImageModel} from '#/state/models/media/image'
 import {GalleryModel} from '#/state/models/media/gallery'
 
@@ -20,7 +19,7 @@ export interface ConfirmModal {
 
 export interface EditProfileModal {
   name: 'edit-profile'
-  profileView: ProfileModel
+  profile: AppBskyActorDefs.ProfileViewDetailed
   onUpdate?: () => void
 }
 
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 8ef322db5..9ce9b6635 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -1,7 +1,6 @@
-import {AppBskyEmbedRecord} from '@atproto/api'
+import {AppBskyEmbedRecord, AppBskyActorDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
 import {makeAutoObservable, runInAction} from 'mobx'
-import {ProfileModel} from '../content/profile'
 import {
   shouldRequestEmailConfirmation,
   setEmailConfirmationRequested,
@@ -18,7 +17,7 @@ interface LightboxModel {}
 
 export class ProfileImageLightbox implements LightboxModel {
   name = 'profile-image'
-  constructor(public profileView: ProfileModel) {
+  constructor(public profile: AppBskyActorDefs.ProfileViewDetailed) {
     makeAutoObservable(this)
   }
 }
diff --git a/src/state/queries/profile-extra-info.ts b/src/state/queries/profile-extra-info.ts
new file mode 100644
index 000000000..54b19c89a
--- /dev/null
+++ b/src/state/queries/profile-extra-info.ts
@@ -0,0 +1,31 @@
+import {useQuery} from '@tanstack/react-query'
+import {useSession} from '../session'
+
+export const RQKEY = (did: string) => ['profile-extra-info', did]
+
+/**
+ * Fetches some additional information for the profile screen which
+ * is not available in the API's ProfileView
+ */
+export function useProfileExtraInfoQuery(did: string) {
+  const {agent} = useSession()
+  return useQuery({
+    queryKey: RQKEY(did),
+    async queryFn() {
+      const [listsRes, feedsRes] = await Promise.all([
+        agent.app.bsky.graph.getLists({
+          actor: did,
+          limit: 1,
+        }),
+        agent.app.bsky.feed.getActorFeeds({
+          actor: did,
+          limit: 1,
+        }),
+      ])
+      return {
+        hasLists: listsRes.data.lists.length > 0,
+        hasFeeds: feedsRes.data.feeds.length > 0,
+      }
+    },
+  })
+}
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 1bd28d5b1..63367b261 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -1,14 +1,23 @@
-import {AtUri} from '@atproto/api'
-import {useQuery, useMutation} from '@tanstack/react-query'
+import {
+  AtUri,
+  AppBskyActorDefs,
+  AppBskyActorProfile,
+  AppBskyActorGetProfile,
+  BskyAgent,
+} from '@atproto/api'
+import {useQuery, useQueryClient, useMutation} from '@tanstack/react-query'
+import {Image as RNImage} from 'react-native-image-crop-picker'
 import {useSession} from '../session'
 import {updateProfileShadow} from '../cache/profile-shadow'
+import {uploadBlob} from '#/lib/api'
+import {until} from '#/lib/async/until'
 
 export const RQKEY = (did: string) => ['profile', did]
 
 export function useProfileQuery({did}: {did: string | undefined}) {
   const {agent} = useSession()
   return useQuery({
-    queryKey: RQKEY(did),
+    queryKey: RQKEY(did || ''),
     queryFn: async () => {
       const res = await agent.getProfile({actor: did || ''})
       return res.data
@@ -17,6 +26,77 @@ export function useProfileQuery({did}: {did: string | undefined}) {
   })
 }
 
+interface ProfileUpdateParams {
+  profile: AppBskyActorDefs.ProfileView
+  updates: AppBskyActorProfile.Record
+  newUserAvatar: RNImage | undefined | null
+  newUserBanner: RNImage | undefined | null
+}
+export function useProfileUpdateMutation() {
+  const {agent} = useSession()
+  const queryClient = useQueryClient()
+  return useMutation<void, Error, ProfileUpdateParams>({
+    mutationFn: async ({profile, updates, newUserAvatar, newUserBanner}) => {
+      await agent.upsertProfile(async existing => {
+        existing = existing || {}
+        existing.displayName = updates.displayName
+        existing.description = updates.description
+        if (newUserAvatar) {
+          const res = await uploadBlob(
+            agent,
+            newUserAvatar.path,
+            newUserAvatar.mime,
+          )
+          existing.avatar = res.data.blob
+        } else if (newUserAvatar === null) {
+          existing.avatar = undefined
+        }
+        if (newUserBanner) {
+          const res = await uploadBlob(
+            agent,
+            newUserBanner.path,
+            newUserBanner.mime,
+          )
+          existing.banner = res.data.blob
+        } else if (newUserBanner === null) {
+          existing.banner = undefined
+        }
+        return existing
+      })
+      await whenAppViewReady(agent, profile.did, res => {
+        if (typeof newUserAvatar !== 'undefined') {
+          if (newUserAvatar === null && res.data.avatar) {
+            // url hasnt cleared yet
+            return false
+          } else if (res.data.avatar === profile.avatar) {
+            // url hasnt changed yet
+            return false
+          }
+        }
+        if (typeof newUserBanner !== 'undefined') {
+          if (newUserBanner === null && res.data.banner) {
+            // url hasnt cleared yet
+            return false
+          } else if (res.data.banner === profile.banner) {
+            // url hasnt changed yet
+            return false
+          }
+        }
+        return (
+          res.data.displayName === updates.displayName &&
+          res.data.description === updates.description
+        )
+      })
+    },
+    onSuccess(data, variables) {
+      // invalidate cache
+      queryClient.invalidateQueries({
+        queryKey: RQKEY(variables.profile.did),
+      })
+    },
+  })
+}
+
 export function useProfileFollowMutation() {
   const {agent} = useSession()
   return useMutation<{uri: string; cid: string}, Error, {did: string}>({
@@ -167,3 +247,16 @@ export function useProfileUnblockMutation() {
     },
   })
 }
+
+async function whenAppViewReady(
+  agent: BskyAgent,
+  actor: string,
+  fn: (res: AppBskyActorGetProfile.Response) => boolean,
+) {
+  await until(
+    5, // 5 tries
+    1e3, // 1s delay between tries
+    fn,
+    () => agent.app.bsky.actor.getProfile({actor}),
+  )
+}