Verification (#8226)
* WIP * Alignment with icon * Add create/remove prompts * Fill out check dialog a bit * Reorg * Handle was verified state * Add warning to edit profile * Add warning to handle dialog * Decent alignment in posts on all platforms * Refactor alignment for posts, chatlist, hover card * Disable on profile * Convo header * Compute simple verification state * Add other icon, rename, integrate * Swap in simple state for profile edits * Clean up utility hooks * Add verifications UI to dialog * Add edu nux * Revert change * Fix wrapping of check on profile * Rename * Fix gap under PostMeta * Update check dialogs * Handle takendown verifiers in check dialog * alf composer reply to * Refactor verification state * Add create/remove mutations, non-functional for now * Fix up post-rebase * Add check to first author noty * Do cache updates after mutations * DRY up hook, add to profile updates too * Add to drawer * Update account list * Adapt to new types * Hook up mutations * Use profile shadow in feeds * Add to settings * Shadow currentAccountProfile * Add invalid state to verifications * Fix alignment and overflow in Settings and Drawer * Re-integrate post rebase * Remove debug code * Update copy * Add unverified notification support * Remove link * Make sure dialog closes * Update URL * Add settings screen * Integrate new setting into verification states * Add metrics, bump package, fix bad import * NUX fixes * Update copy * Fixes * Update types * fix search autocomplete * fix lint * add display name warning to new dialog * update default prefs * Add parsing support for notifications * Bump pkg * Tweak noty styles * Adjust check alignment * Tweak check alignment * Fix badge for verifier * Modify copy --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Vendored
+4
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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],
|
||||
)
|
||||
}
|
||||
@@ -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})
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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})
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user