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/profile-shadow.ts4
-rw-r--r--src/state/queries/notifications/util.ts26
-rw-r--r--src/state/queries/nuxs/definitions.ts6
-rw-r--r--src/state/queries/preferences/const.ts7
-rw-r--r--src/state/queries/preferences/index.ts30
-rw-r--r--src/state/queries/profile.ts31
-rw-r--r--src/state/queries/useCurrentAccountProfile.tsx9
-rw-r--r--src/state/queries/verification/useUpdateProfileVerificationCache.ts35
-rw-r--r--src/state/queries/verification/useVerificationCreateMutation.tsx53
-rw-r--r--src/state/queries/verification/useVerificationsRemoveMutation.tsx63
10 files changed, 233 insertions, 31 deletions
diff --git a/src/state/cache/profile-shadow.ts b/src/state/cache/profile-shadow.ts
index 82ee44388..9c23e4550 100644
--- a/src/state/cache/profile-shadow.ts
+++ b/src/state/cache/profile-shadow.ts
@@ -1,4 +1,5 @@
 import {useEffect, useMemo, useState} from 'react'
+import {type AppBskyActorDefs} from '@atproto/api'
 import {type QueryClient} from '@tanstack/react-query'
 import EventEmitter from 'eventemitter3'
 
@@ -29,6 +30,7 @@ export interface ProfileShadow {
   followingUri: string | undefined
   muted: boolean | undefined
   blockingUri: string | undefined
+  verification: AppBskyActorDefs.VerificationState
 }
 
 const shadows: WeakMap<
@@ -134,6 +136,8 @@ function mergeShadow<TProfileView extends bsky.profile.AnyProfileView>(
       blocking:
         'blockingUri' in shadow ? shadow.blockingUri : profile.viewer?.blocking,
     },
+    verification:
+      'verification' in shadow ? shadow.verification : profile.verification,
   })
 }
 
diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts
index f6f53f58f..6bbf9b250 100644
--- a/src/state/queries/notifications/util.ts
+++ b/src/state/queries/notifications/util.ts
@@ -1,22 +1,26 @@
 import {
-  AppBskyFeedDefs,
+  type AppBskyFeedDefs,
   AppBskyFeedLike,
   AppBskyFeedPost,
   AppBskyFeedRepost,
-  AppBskyGraphDefs,
+  type AppBskyGraphDefs,
   AppBskyGraphStarterpack,
-  AppBskyNotificationListNotifications,
-  BskyAgent,
+  type AppBskyNotificationListNotifications,
+  type BskyAgent,
   moderateNotification,
-  ModerationOpts,
+  type ModerationOpts,
 } from '@atproto/api'
-import {QueryClient} from '@tanstack/react-query'
+import {type QueryClient} from '@tanstack/react-query'
 import chunk from 'lodash.chunk'
 
 import {labelIsHideableOffense} from '#/lib/moderation'
 import * as bsky from '#/types/bsky'
 import {precacheProfile} from '../profile'
-import {FeedNotification, FeedPage, NotificationType} from './types'
+import {
+  type FeedNotification,
+  type FeedPage,
+  type NotificationType,
+} from './types'
 
 const GROUPABLE_REASONS = ['like', 'repost', 'follow']
 const MS_1HR = 1e3 * 60 * 60
@@ -155,14 +159,14 @@ export function groupNotifications(
       const type = toKnownType(notif)
       if (type !== 'starterpack-joined') {
         groupedNotifs.push({
-          _reactKey: `notif-${notif.uri}`,
+          _reactKey: `notif-${notif.uri}-${notif.reason}`,
           type,
           notification: notif,
           subjectUri: getSubjectUri(type, notif),
         })
       } else {
         groupedNotifs.push({
-          _reactKey: `notif-${notif.uri}`,
+          _reactKey: `notif-${notif.uri}-${notif.reason}`,
           type: 'starterpack-joined',
           notification: notif,
           subjectUri: notif.uri,
@@ -238,7 +242,9 @@ function toKnownType(
     notif.reason === 'reply' ||
     notif.reason === 'quote' ||
     notif.reason === 'follow' ||
-    notif.reason === 'starterpack-joined'
+    notif.reason === 'starterpack-joined' ||
+    notif.reason === 'verified' ||
+    notif.reason === 'unverified'
   ) {
     return notif.reason as NotificationType
   }
diff --git a/src/state/queries/nuxs/definitions.ts b/src/state/queries/nuxs/definitions.ts
index 8eb53a0a4..a44ffa4c5 100644
--- a/src/state/queries/nuxs/definitions.ts
+++ b/src/state/queries/nuxs/definitions.ts
@@ -5,6 +5,7 @@ import {type BaseNux} from '#/state/queries/nuxs/types'
 export enum Nux {
   NeueTypography = 'NeueTypography',
   ExploreInterestsCard = 'ExploreInterestsCard',
+  InitialVerificationAnnouncement = 'InitialVerificationAnnouncement',
 }
 
 export const nuxNames = new Set(Object.values(Nux))
@@ -18,9 +19,14 @@ export type AppNux = BaseNux<
       id: Nux.ExploreInterestsCard
       data: undefined
     }
+  | {
+      id: Nux.InitialVerificationAnnouncement
+      data: undefined
+    }
 >
 
 export const NuxSchemas: Record<Nux, zod.ZodObject<any> | undefined> = {
   [Nux.NeueTypography]: undefined,
   [Nux.ExploreInterestsCard]: undefined,
+  [Nux.InitialVerificationAnnouncement]: undefined,
 }
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts
index 3c1fead5e..84b208a9f 100644
--- a/src/state/queries/preferences/const.ts
+++ b/src/state/queries/preferences/const.ts
@@ -1,7 +1,7 @@
 import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
 import {
-  ThreadViewPreferences,
-  UsePreferencesQueryResponse,
+  type ThreadViewPreferences,
+  type UsePreferencesQueryResponse,
 } from '#/state/queries/preferences/types'
 
 export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
@@ -43,4 +43,7 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
     threadgateAllowRules: undefined,
     postgateEmbeddingRules: [],
   },
+  verificationPrefs: {
+    hideBadges: false,
+  },
 }
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index 81b3dd086..daab5eca3 100644
--- a/src/state/queries/preferences/index.ts
+++ b/src/state/queries/preferences/index.ts
@@ -1,7 +1,7 @@
 import {
-  AppBskyActorDefs,
-  BskyFeedViewPreference,
-  LabelPreference,
+  type AppBskyActorDefs,
+  type BskyFeedViewPreference,
+  type LabelPreference,
 } from '@atproto/api'
 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
 
@@ -16,8 +16,8 @@ import {
   DEFAULT_THREAD_VIEW_PREFS,
 } from '#/state/queries/preferences/const'
 import {
-  ThreadViewPreferences,
-  UsePreferencesQueryResponse,
+  type ThreadViewPreferences,
+  type UsePreferencesQueryResponse,
 } from '#/state/queries/preferences/types'
 import {useAgent} from '#/state/session'
 import {saveLabelers} from '#/state/session/agent-config'
@@ -407,3 +407,23 @@ export function useSetActiveProgressGuideMutation() {
     },
   })
 }
+
+export function useSetVerificationPrefsMutation() {
+  const queryClient = useQueryClient()
+  const agent = useAgent()
+
+  return useMutation<void, unknown, AppBskyActorDefs.VerificationPrefs>({
+    mutationFn: async prefs => {
+      await agent.setVerificationPrefs(prefs)
+      if (prefs.hideBadges) {
+        logger.metric('verification:settings:hideBadges', {})
+      } else {
+        logger.metric('verification:settings:unHideBadges', {})
+      }
+      // triggers a refetch
+      await queryClient.invalidateQueries({
+        queryKey: preferencesQueryKey,
+      })
+    },
+  })
+}
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 2cf144d3a..609a62e25 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -1,18 +1,18 @@
 import {useCallback} from 'react'
-import {Image as RNImage} from 'react-native-image-crop-picker'
+import {type Image as RNImage} from 'react-native-image-crop-picker'
 import {
-  AppBskyActorDefs,
-  AppBskyActorGetProfile,
-  AppBskyActorGetProfiles,
-  AppBskyActorProfile,
+  type AppBskyActorDefs,
+  type AppBskyActorGetProfile,
+  type AppBskyActorGetProfiles,
+  type AppBskyActorProfile,
   AtUri,
-  BskyAgent,
-  ComAtprotoRepoUploadBlob,
-  Un$Typed,
+  type BskyAgent,
+  type ComAtprotoRepoUploadBlob,
+  type Un$Typed,
 } from '@atproto/api'
 import {
   keepPreviousData,
-  QueryClient,
+  type QueryClient,
   useMutation,
   useQuery,
   useQueryClient,
@@ -21,16 +21,17 @@ import {
 import {uploadBlob} from '#/lib/api'
 import {until} from '#/lib/async/until'
 import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
-import {logEvent, LogEvents, toClout} from '#/lib/statsig/statsig'
-import {Shadow} from '#/state/cache/types'
+import {logEvent, type LogEvents, toClout} from '#/lib/statsig/statsig'
+import {type Shadow} from '#/state/cache/types'
 import {STALE} from '#/state/queries'
 import {resetProfilePostsQueries} from '#/state/queries/post-feed'
 import {
   unstableCacheProfileView,
   useUnstableProfileViewCache,
 } from '#/state/queries/unstable-profile-cache'
+import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache'
 import * as userActionHistory from '#/state/userActionHistory'
-import * as bsky from '#/types/bsky'
+import type * as bsky from '#/types/bsky'
 import {updateProfileShadow} from '../cache/profile-shadow'
 import {useAgent, useSession} from '../session'
 import {
@@ -50,7 +51,7 @@ export const precacheProfile = unstableCacheProfileView
 const RQKEY_ROOT = 'profile'
 export const RQKEY = (did: string) => [RQKEY_ROOT, did]
 
-const profilesQueryKeyRoot = 'profiles'
+export const profilesQueryKeyRoot = 'profiles'
 export const profilesQueryKey = (handles: string[]) => [
   profilesQueryKeyRoot,
   handles,
@@ -137,6 +138,7 @@ interface ProfileUpdateParams {
 export function useProfileUpdateMutation() {
   const queryClient = useQueryClient()
   const agent = useAgent()
+  const updateProfileVerificationCache = useUpdateProfileVerificationCache()
   return useMutation<void, Error, ProfileUpdateParams>({
     mutationFn: async ({
       profile,
@@ -223,7 +225,7 @@ export function useProfileUpdateMutation() {
           }),
       )
     },
-    onSuccess(data, variables) {
+    async onSuccess(_, variables) {
       // invalidate cache
       queryClient.invalidateQueries({
         queryKey: RQKEY(variables.profile.did),
@@ -231,6 +233,7 @@ export function useProfileUpdateMutation() {
       queryClient.invalidateQueries({
         queryKey: [profilesQueryKeyRoot, [variables.profile.did]],
       })
+      await updateProfileVerificationCache({profile: variables.profile})
     },
   })
 }
diff --git a/src/state/queries/useCurrentAccountProfile.tsx b/src/state/queries/useCurrentAccountProfile.tsx
new file mode 100644
index 000000000..d1f562efc
--- /dev/null
+++ b/src/state/queries/useCurrentAccountProfile.tsx
@@ -0,0 +1,9 @@
+import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
+import {useProfileQuery} from '#/state/queries/profile'
+import {useSession} from '#/state/session'
+
+export function useCurrentAccountProfile() {
+  const {currentAccount} = useSession()
+  const {data: profile} = useProfileQuery({did: currentAccount?.did})
+  return useMaybeProfileShadow(profile)
+}
diff --git a/src/state/queries/verification/useUpdateProfileVerificationCache.ts b/src/state/queries/verification/useUpdateProfileVerificationCache.ts
new file mode 100644
index 000000000..f5ccf1458
--- /dev/null
+++ b/src/state/queries/verification/useUpdateProfileVerificationCache.ts
@@ -0,0 +1,35 @@
+import {useCallback} from 'react'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {updateProfileShadow} from '#/state/cache/profile-shadow'
+import {useAgent} from '#/state/session'
+import type * as bsky from '#/types/bsky'
+
+/**
+ * Fetches a fresh verification state from the app view and updates our profile
+ * cache. This state is computed using a variety of factors on the server, so
+ * we need to get this data from the server.
+ */
+export function useUpdateProfileVerificationCache() {
+  const qc = useQueryClient()
+  const agent = useAgent()
+
+  return useCallback(
+    async ({profile}: {profile: bsky.profile.AnyProfileView}) => {
+      try {
+        const {data: updated} = await agent.getProfile({
+          actor: profile.did ?? '',
+        })
+        updateProfileShadow(qc, profile.did, {
+          verification: updated.verification,
+        })
+      } catch (e) {
+        logger.error(`useUpdateProfileVerificationCache failed`, {
+          safeMessage: e,
+        })
+      }
+    },
+    [agent, qc],
+  )
+}
diff --git a/src/state/queries/verification/useVerificationCreateMutation.tsx b/src/state/queries/verification/useVerificationCreateMutation.tsx
new file mode 100644
index 000000000..1048eb9d2
--- /dev/null
+++ b/src/state/queries/verification/useVerificationCreateMutation.tsx
@@ -0,0 +1,53 @@
+import {type AppBskyActorGetProfile} from '@atproto/api'
+import {useMutation} from '@tanstack/react-query'
+
+import {until} from '#/lib/async/until'
+import {logger} from '#/logger'
+import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache'
+import {useAgent, useSession} from '#/state/session'
+import type * as bsky from '#/types/bsky'
+
+export function useVerificationCreateMutation() {
+  const agent = useAgent()
+  const {currentAccount} = useSession()
+  const updateProfileVerificationCache = useUpdateProfileVerificationCache()
+
+  return useMutation({
+    async mutationFn({profile}: {profile: bsky.profile.AnyProfileView}) {
+      if (!currentAccount) {
+        throw new Error('User not logged in')
+      }
+
+      const {uri} = await agent.app.bsky.graph.verification.create(
+        {repo: currentAccount.did},
+        {
+          subject: profile.did,
+          createdAt: new Date().toISOString(),
+          handle: profile.handle,
+          displayName: profile.displayName || '',
+        },
+      )
+
+      await until(
+        5,
+        1e3,
+        ({data: profile}: AppBskyActorGetProfile.Response) => {
+          if (
+            profile.verification &&
+            profile.verification.verifications.find(v => v.uri === uri)
+          ) {
+            return true
+          }
+          return false
+        },
+        () => {
+          return agent.getProfile({actor: profile.did ?? ''})
+        },
+      )
+    },
+    async onSuccess(_, {profile}) {
+      logger.metric('verification:create', {})
+      await updateProfileVerificationCache({profile})
+    },
+  })
+}
diff --git a/src/state/queries/verification/useVerificationsRemoveMutation.tsx b/src/state/queries/verification/useVerificationsRemoveMutation.tsx
new file mode 100644
index 000000000..936c786c9
--- /dev/null
+++ b/src/state/queries/verification/useVerificationsRemoveMutation.tsx
@@ -0,0 +1,63 @@
+import {
+  type AppBskyActorDefs,
+  type AppBskyActorGetProfile,
+  AtUri,
+} from '@atproto/api'
+import {useMutation} from '@tanstack/react-query'
+
+import {until} from '#/lib/async/until'
+import {logger} from '#/logger'
+import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache'
+import {useAgent, useSession} from '#/state/session'
+import type * as bsky from '#/types/bsky'
+
+export function useVerificationsRemoveMutation() {
+  const agent = useAgent()
+  const {currentAccount} = useSession()
+  const updateProfileVerificationCache = useUpdateProfileVerificationCache()
+
+  return useMutation({
+    async mutationFn({
+      profile,
+      verifications,
+    }: {
+      profile: bsky.profile.AnyProfileView
+      verifications: AppBskyActorDefs.VerificationView[]
+    }) {
+      if (!currentAccount) {
+        throw new Error('User not logged in')
+      }
+
+      const uris = verifications.map(v => v.uri)
+
+      await Promise.all(
+        uris.map(uri => {
+          return agent.app.bsky.graph.verification.delete({
+            repo: currentAccount.did,
+            rkey: new AtUri(uri).rkey,
+          })
+        }),
+      )
+
+      await until(
+        5,
+        1e3,
+        ({data: profile}: AppBskyActorGetProfile.Response) => {
+          if (
+            !profile.verification?.verifications.some(v => uris.includes(v.uri))
+          ) {
+            return true
+          }
+          return false
+        },
+        () => {
+          return agent.getProfile({actor: profile.did ?? ''})
+        },
+      )
+    },
+    async onSuccess(_, {profile}) {
+      logger.metric('verification:revoke', {})
+      await updateProfileVerificationCache({profile})
+    },
+  })
+}