diff --git a/src/components/dialogs/SearchablePeopleList.tsx b/src/components/dialogs/SearchablePeopleList.tsx
index 16ab49ea7..540ce29d4 100644
--- a/src/components/dialogs/SearchablePeopleList.tsx
+++ b/src/components/dialogs/SearchablePeopleList.tsx
@@ -77,12 +77,14 @@ export function SearchablePeopleList({
title,
showRecentConvos,
sortByMessageDeclaration,
+ excludeSelf = true,
onSelectChat,
renderProfileCard,
}: {
title: string
showRecentConvos?: boolean
sortByMessageDeclaration?: boolean
+ excludeSelf?: boolean
} & (
| {
renderProfileCard: (item: ProfileItem) => React.ReactNode
@@ -131,7 +133,7 @@ export function SearchablePeopleList({
} else if (searchText.length) {
if (results?.length) {
for (const profile of results) {
- if (profile.did === currentAccount?.did) continue
+ if (excludeSelf && profile.did === currentAccount?.did) continue
_items.push({
type: 'profile',
key: profile.did,
@@ -242,6 +244,7 @@ export function SearchablePeopleList({
results,
isError,
currentAccount?.did,
+ excludeSelf,
follows,
convos,
showRecentConvos,
diff --git a/src/screens/Settings/RunesSettings/BadgesSettings.tsx b/src/screens/Settings/RunesSettings/BadgesSettings.tsx
index 96e381886..718ad4d6d 100644
--- a/src/screens/Settings/RunesSettings/BadgesSettings.tsx
+++ b/src/screens/Settings/RunesSettings/BadgesSettings.tsx
@@ -1,15 +1,21 @@
-import {useState} from 'react'
-import {View} from 'react-native'
-import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs'
-import {Trans, useLingui} from '@lingui/react/macro'
+import {useCallback, useMemo, useState} from 'react'
+import {LayoutAnimation, View} from 'react-native'
+import {useReducedMotion} from 'react-native-reanimated'
+import {type ModerationOpts} from '@atproto/api'
+import {Plural, Trans, useLingui} from '@lingui/react/macro'
import {usePalette} from '#/lib/hooks/usePalette'
import * as persisted from '#/state/persisted'
import {
+ useDeerVerification,
useDeerVerificationEnabled,
+ useDeerVerificationTrustAppView,
useDeerVerificationTrusted,
+ useSetDeerVerification,
useSetDeerVerificationEnabled,
+ useSetDeerVerificationTrust,
} from '#/state/preferences/deer-verification'
+import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
import {
useFaviconService,
useSetFaviconService,
@@ -22,26 +28,43 @@ import {
useSetPdsLabelHideBskyPds,
} from '#/state/preferences/pds-label'
import {useProfilesQuery} from '#/state/queries/profile'
+import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
+import {useSession} from '#/state/session'
import * as SettingsList from '#/screens/Settings/components/SettingsList'
-import {atoms as a, useBreakpoints} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
-import {Button, ButtonText} from '#/components/Button'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
+import {
+ type ProfileItem,
+ SearchablePeopleList,
+} from '#/components/dialogs/SearchablePeopleList'
import * as Toggle from '#/components/forms/Toggle'
+import {
+ ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon,
+ ChevronTop_Stroke2_Corner0_Rounded as ChevronTopIcon,
+} from '#/components/icons/Chevron'
import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller'
+import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star'
+import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified'
+import * as ProfileCard from '#/components/ProfileCard'
+import * as Prompt from '#/components/Prompt'
+import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {IS_WEB} from '#/env'
-import {SearchProfileCard} from '../../Search/components/SearchProfileCard'
+import type * as bsky from '#/types/bsky'
import {RunesScreenLayout} from './components/RunesScreenLayout'
export function RunesBadgesSettingsScreen() {
const {t: l} = useLingui()
+ const deerVerification = useDeerVerification()
const deerVerificationEnabled = useDeerVerificationEnabled()
+ const trustAppView = useDeerVerificationTrustAppView()
+ const setDeerVerification = useSetDeerVerification()
const setDeerVerificationEnabled = useSetDeerVerificationEnabled()
- const setTrustedVerifiersDialogControl = Dialog.useDialogControl()
const pdsLabelEnabled = usePdsLabelEnabled()
const setPdsLabelEnabled = useSetPdsLabelEnabled()
@@ -51,19 +74,38 @@ export function RunesBadgesSettingsScreen() {
return (
- setDeerVerificationEnabled(value)}>
-
-
-
- Select trusted verifiers and act as one
-
+
+
+
+ Trusted verifiers
+
+ setDeerVerificationEnabled(value)}
+ style={[a.w_full]}>
+
+ Use own selection of trusted verifiers
+
-
-
+
+ {deerVerificationEnabled && (
+
+ setDeerVerification({...deerVerification, trustAppView: value})
+ }
+ style={[a.w_full]}>
+
+ Trust verifiers from current AppView
+
+
+
+ )}
+
@@ -76,16 +118,7 @@ export function RunesBadgesSettingsScreen() {
-
-
-
- {`Trusted Verifiers`}
-
- setTrustedVerifiersDialogControl.open()}
- />
-
+
@@ -133,7 +166,6 @@ export function RunesBadgesSettingsScreen() {
)}
-
)
}
@@ -231,53 +263,312 @@ function FaviconServiceDialog({control}: {control: Dialog.DialogControlProps}) {
)
}
-function TrustedVerifiersDialog({
- control,
-}: {
- control: Dialog.DialogControlProps
-}) {
+function TrustedVerifiersSection() {
const {t: l} = useLingui()
+ const reducedMotion = useReducedMotion()
+ const addTrustedVerifierDialogControl = Dialog.useDialogControl()
+ const resetTrustedVerifiersPromptControl = Prompt.usePromptControl()
+ const deerVerification = useDeerVerification()
+ const setDeerVerification = useSetDeerVerification()
+ const currentAccountProfile = useCurrentAccountProfile()
+ const {currentAccount} = useSession()
+ const currentAccountDid = currentAccount?.did
+ const [isExpanded, setIsExpanded] = useState(false)
+ const trusted = useDeerVerificationTrusted()
+
+ const results = useProfilesQuery({
+ handles: Array.from(trusted).filter(
+ did => did !== currentAccountProfile?.did,
+ ),
+ maintainData: true,
+ })
+
+ const trustedProfiles = useMemo(() => {
+ const profiles: bsky.profile.AnyProfileView[] = []
+ const seen = new Set()
+
+ if (currentAccountProfile && trusted.has(currentAccountProfile.did)) {
+ profiles.push(currentAccountProfile)
+ seen.add(currentAccountProfile.did)
+ }
+
+ for (const profile of results.data?.profiles ?? []) {
+ if (!trusted.has(profile.did)) continue
+ if (seen.has(profile.did)) continue
+ profiles.push(profile)
+ seen.add(profile.did)
+ }
+
+ return profiles
+ }, [currentAccountProfile, results.data?.profiles, trusted])
+
+ const onToggleExpanded = useCallback(() => {
+ if (!reducedMotion) {
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
+ }
+ setIsExpanded(current => !current)
+ }, [reducedMotion])
+
+ const onResetAll = useCallback(() => {
+ setDeerVerification({
+ ...deerVerification,
+ trustedSelf: true,
+ trusted: [],
+ })
+ }, [deerVerification, setDeerVerification])
return (
-
-
-
-
-
- Trusted Verifiers
-
-
+ <>
+
+
+
+ {`Trusted Verifiers`}
+
+ {!isExpanded && (
+
+
+
+ )}
+
+
-
+ {isExpanded && (
+
+ {trustedProfiles.length > 0 ? (
+ trustedProfiles.map(profile => (
+
+ ))
+ ) : (
+
+
+ No trusted verifiers selected.
+
+
+ )}
-
-
-
+
+
+
+
+
+ )}
+
+
+
+ >
)
}
-function TrustedVerifiers() {
- const trusted = useDeerVerificationTrusted()
+function TrustedVerifierRow({
+ profile,
+ isCurrentAccount,
+}: {
+ profile: bsky.profile.AnyProfileView
+ isCurrentAccount: boolean
+}) {
+ const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
- const {gtMobile} = useBreakpoints()
+ const enableSquareButtons = useEnableSquareButtons()
+ const setDeerVerificationTrust = useSetDeerVerificationTrust()
+ const t = useTheme()
- const results = useProfilesQuery({
- handles: Array.from(trusted),
- })
+ const onRemove = useCallback(() => {
+ setDeerVerificationTrust.remove(profile.did)
+ Toast.show(l`Removed trusted verifier`)
+ }, [l, profile.did, setDeerVerificationTrust])
- if (!results.data || moderationOpts === undefined) {
- return null
- }
+ if (!moderationOpts) return null
+
+ return (
+
+
+
+
+
+
+
+
+ {isCurrentAccount && (
+
+ Current account
+
+ )}
+
+
+
+
+
+
+
+ )
+}
+function TrustedVerifiersAddDialog({
+ control,
+}: {
+ control: Dialog.DialogControlProps
+}) {
return (
-
- {results.data.profiles.map(profile => (
-
+
+
+
+ )
+}
+
+function TrustedVerifiersAddDialogInner() {
+ const {t: l} = useLingui()
+ const moderationOpts = useModerationOpts()
+ const trusted = useDeerVerificationTrusted()
+ const setDeerVerificationTrust = useSetDeerVerificationTrust()
+ const {currentAccount} = useSession()
+
+ const renderProfileCard = useCallback(
+ (item: ProfileItem) => {
+ if (!moderationOpts) return null
+
+ return (
+ {
+ if (trusted.has(item.profile.did)) {
+ setDeerVerificationTrust.remove(item.profile.did)
+ Toast.show(l`Removed trusted verifier`)
+ } else {
+ setDeerVerificationTrust.add(item.profile.did)
+ Toast.show(l`Added trusted verifier`)
+ }
+ }}
/>
- ))}
+ )
+ },
+ [currentAccount?.did, l, moderationOpts, setDeerVerificationTrust, trusted],
+ )
+
+ return (
+
+ )
+}
+
+function TrustedVerifierSearchResult({
+ profile,
+ moderationOpts,
+ isTrusted,
+ isCurrentAccount,
+ onToggle,
+}: {
+ profile: bsky.profile.AnyProfileView
+ moderationOpts: ModerationOpts
+ isTrusted: boolean
+ isCurrentAccount: boolean
+ onToggle: () => void
+}) {
+ const {t: l} = useLingui()
+ const t = useTheme()
+
+ return (
+
+
+
+
+
+
+
+
+ {isCurrentAccount && (
+
+ Current account
+
+ )}
+
+
+
+
+
+
)
}
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 62645fe55..e9d355ce5 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -200,6 +200,8 @@ const schema = z.object({
deerVerification: z
.object({
enabled: z.boolean(),
+ trustAppView: z.boolean().optional(),
+ trustedSelf: z.boolean().optional(),
trusted: z.array(z.string()),
})
.optional(),
@@ -330,30 +332,9 @@ export const defaults: Schema = {
showViaClient: true,
deerVerification: {
enabled: false,
- // https://witchsky.app/profile/did:plc:p2cp5gopk7mgjegy6wadk3ep/post/3lndyqyyr4k2k
- // using https://bverified.vercel.app/trusted as a source
- trusted: [
- 'did:plc:z72i7hdynmk6r22z27h6tvur',
- 'did:plc:b2kutgxqlltwc6lhs724cfwr',
- 'did:plc:inz4fkbbp7ms3ixufw6xuvdi',
- 'did:plc:eclio37ymobqex2ncko63h4r',
- 'did:plc:dzezcmpb3fhcpns4n4xm4ur5',
- 'did:plc:5u54z2qgkq43dh2nzwzdbbhb',
- 'did:plc:wmho6q2uiyktkam3jsvrms3s',
- 'did:plc:sqbswn3lalcc2dlh2k7zdpuw',
- 'did:plc:k5nskatzhyxersjilvtnz4lh',
- 'did:plc:d2jith367s6ybc3ldsusgdae',
- 'did:plc:y3xrmnwvkvsq4tqcsgwch4na',
- 'did:plc:i3fhjvvkbmirhyu4aeihhrnv',
- 'did:plc:fivojrvylkim4nuo3pfqcf3k',
- 'did:plc:ofbkqcjzvm6gtwuufsubnkaf',
- 'did:plc:xwqgusybtrpm67tcwqdfmzvy',
- 'did:plc:oxo226vi7t2btjokm2buusoy',
- 'did:plc:r4ve5hjtfjubdwrvlxcad62e',
- 'did:plc:j4eroku3volozvv6ljsnnfec',
- 'did:plc:6q2thhy2ohzog26mmqm4pffk',
- 'did:plc:rk25gdgk3cnnmtkvlae265nz',
- ],
+ trustAppView: true,
+ trustedSelf: true,
+ trusted: [],
},
highQualityImages: false,
imageCdnHost: 'https://cdn.bsky.app',
diff --git a/src/state/preferences/deer-verification.tsx b/src/state/preferences/deer-verification.tsx
index 196b1565f..4ccc89154 100644
--- a/src/state/preferences/deer-verification.tsx
+++ b/src/state/preferences/deer-verification.tsx
@@ -9,6 +9,7 @@ import {
} from 'react'
import * as persisted from '#/state/persisted'
+import {useSession} from '#/state/session'
type StateContext = persisted.Schema['deerVerification']
type SetContext = (v: persisted.Schema['deerVerification']) => void
@@ -26,7 +27,7 @@ export function Provider({children}: PropsWithChildren<{}>) {
const setStateWrapped = useCallback(
(deerVerification: persisted.Schema['deerVerification']) => {
setState(deerVerification)
- persisted.write('deerVerification', deerVerification)
+ void persisted.write('deerVerification', deerVerification)
},
[setState],
)
@@ -54,14 +55,23 @@ export function useDeerVerificationEnabled() {
return useDeerVerification().enabled
}
-export function useDeerVerificationTrusted(
- mandatory: string | undefined = undefined,
-) {
- const trusted = new Set(useDeerVerification().trusted)
- if (mandatory) {
- trusted.add(mandatory)
- }
- return trusted
+export function useDeerVerificationTrustAppView() {
+ return useDeerVerification().trustAppView ?? true
+}
+
+export function useDeerVerificationTrusted() {
+ const deerVerification = useDeerVerification()
+ const {currentAccount} = useSession()
+ const currentAccountDid = currentAccount?.did
+ const trustedSelf = deerVerification.trustedSelf ?? true
+
+ return useMemo(() => {
+ const trusted = new Set(deerVerification.trusted)
+ if (trustedSelf && currentAccountDid) {
+ trusted.add(currentAccountDid)
+ }
+ return trusted
+ }, [currentAccountDid, deerVerification.trusted, trustedSelf])
}
export function useSetDeerVerification() {
@@ -82,20 +92,50 @@ export function useSetDeerVerificationEnabled() {
export function useSetDeerVerificationTrust() {
const deerVerification = useDeerVerification()
const setDeerVerification = useSetDeerVerification()
+ const {currentAccount} = useSession()
+ const currentAccountDid = currentAccount?.did
return useMemo(
() => ({
add: (add: string) => {
const trusted = new Set(deerVerification.trusted)
+ if (add === currentAccountDid) {
+ trusted.delete(add)
+ setDeerVerification({
+ ...deerVerification,
+ trustedSelf: true,
+ trusted: Array.from(trusted),
+ })
+ return
+ }
+
trusted.add(add)
- setDeerVerification({...deerVerification, trusted: Array.from(trusted)})
+ setDeerVerification({
+ ...deerVerification,
+ trustedSelf: deerVerification.trustedSelf ?? true,
+ trusted: Array.from(trusted),
+ })
},
remove: (remove: string) => {
const trusted = new Set(deerVerification.trusted)
+ if (remove === currentAccountDid) {
+ trusted.delete(remove)
+ setDeerVerification({
+ ...deerVerification,
+ trustedSelf: false,
+ trusted: Array.from(trusted),
+ })
+ return
+ }
+
trusted.delete(remove)
- setDeerVerification({...deerVerification, trusted: Array.from(trusted)})
+ setDeerVerification({
+ ...deerVerification,
+ trustedSelf: deerVerification.trustedSelf ?? true,
+ trusted: Array.from(trusted),
+ })
},
}),
- [deerVerification, setDeerVerification],
+ [currentAccountDid, deerVerification, setDeerVerification],
)
}
diff --git a/src/state/queries/deer-verification.ts b/src/state/queries/deer-verification.ts
index 98fd706fc..29ed8ce81 100644
--- a/src/state/queries/deer-verification.ts
+++ b/src/state/queries/deer-verification.ts
@@ -11,6 +11,7 @@ import {type AnyProfileView} from '#/types/bsky/profile'
import {useConstellationInstance} from '../preferences/constellation-instance'
import {
useDeerVerificationEnabled,
+ useDeerVerificationTrustAppView,
useDeerVerificationTrusted,
} from '../preferences/deer-verification'
import {
@@ -24,7 +25,6 @@ import {
} from './constellation'
import {LRU} from './direct-fetch-record'
import {resolvePdsServiceUrl} from './resolve-identity'
-import {useCurrentAccountProfile} from './useCurrentAccountProfile'
const RQKEY_ROOT = 'deer-verification'
export const RQKEY = (did: string, trusted: Set) => [
@@ -53,8 +53,8 @@ export function getTrustedConstellationVerifications(
from_dids: Array.from(trusted),
})
return asyncGenDedupe(
- asyncGenFilter(verificationLinks, (link) => trusted.has(link.did)),
- (link) => link.did,
+ asyncGenFilter(verificationLinks, link => trusted.has(link.did)),
+ link => link.did,
)
}
@@ -84,7 +84,9 @@ async function getDeerVerificationLinkedRecords(
const record = await verificationCache.getOrTryInsertWith(
request,
async () => {
- const resp = await (await fetch(request)).json()
+ const resp = (await (await fetch(request)).json()) as {
+ value?: unknown
+ }
return resp.value
},
)
@@ -124,11 +126,41 @@ function createVerificationViews(
}))
}
+function mergeVerificationViews(
+ appViewVerifications: VerificationView[],
+ deerVerifications: VerificationView[],
+) {
+ const merged = new Map()
+
+ for (const verification of appViewVerifications) {
+ merged.set(verification.uri, verification)
+ }
+
+ for (const verification of deerVerifications) {
+ merged.set(verification.uri, verification)
+ }
+
+ return Array.from(merged.values())
+}
+
function createVerificationState(
- verifications: VerificationView[],
+ appViewVerifications: VerificationView[],
+ deerVerifications: VerificationView[],
profile: AnyProfileView,
trusted: Set,
+ trustAppView: boolean,
): VerificationState {
+ const verifications = mergeVerificationViews(
+ appViewVerifications,
+ deerVerifications,
+ )
+ const appViewTrustedVerifierStatus = trustAppView
+ ? profile.verification?.trustedVerifierStatus
+ : undefined
+ const trustedVerifierStatus = trusted.has(profile.did)
+ ? 'valid'
+ : (appViewTrustedVerifierStatus ?? 'none')
+
return {
verifications,
verifiedStatus:
@@ -137,7 +169,7 @@ function createVerificationState(
? 'valid'
: 'invalid'
: 'none',
- trustedVerifierStatus: trusted.has(profile.did) ? 'valid' : 'none',
+ trustedVerifierStatus,
}
}
@@ -149,8 +181,8 @@ export function useDeerVerificationState({
enabled?: boolean
}) {
const instance = useConstellationInstance()
- const currentAccountProfile = useCurrentAccountProfile()
- const trusted = useDeerVerificationTrusted(currentAccountProfile?.did)
+ const trusted = useDeerVerificationTrusted()
+ const trustAppView = useDeerVerificationTrustAppView()
const linkedRecords = useQuery({
staleTime: STALE.HOURS.ONE,
@@ -168,11 +200,13 @@ export function useDeerVerificationState({
})
if (linkedRecords.data === undefined || profile === undefined) return
- const verifications = createVerificationViews(linkedRecords.data, profile)
+ const deerVerifications = createVerificationViews(linkedRecords.data, profile)
const verificationState = createVerificationState(
- verifications,
+ trustAppView ? (profile.verification?.verifications ?? []) : [],
+ deerVerifications,
profile,
trusted,
+ trustAppView,
)
return verificationState
diff --git a/src/state/queries/verification/useVerificationCreateMutation.tsx b/src/state/queries/verification/useVerificationCreateMutation.tsx
index 14eefb7c2..844b35271 100644
--- a/src/state/queries/verification/useVerificationCreateMutation.tsx
+++ b/src/state/queries/verification/useVerificationCreateMutation.tsx
@@ -25,9 +25,7 @@ export function useVerificationCreateMutation() {
const qc = useQueryClient()
const deerVerificationEnabled = useDeerVerificationEnabled()
- const deerVerificationTrusted = useDeerVerificationTrusted(
- currentAccount?.did,
- )
+ const deerVerificationTrusted = useDeerVerificationTrusted()
const constellationInstance = useConstellationInstance()
return useMutation({
@@ -86,7 +84,7 @@ export function useVerificationCreateMutation() {
async onSuccess(_, {profile}) {
ax.metric('verification:create', {})
await updateProfileVerificationCache({profile})
- qc.invalidateQueries({
+ await qc.invalidateQueries({
queryKey: DEER_VERIFICATION_RQKEY(profile.did, deerVerificationTrusted),
})
},
diff --git a/src/state/queries/verification/useVerificationsRemoveMutation.tsx b/src/state/queries/verification/useVerificationsRemoveMutation.tsx
index fe4acbfbb..8fa548aaf 100644
--- a/src/state/queries/verification/useVerificationsRemoveMutation.tsx
+++ b/src/state/queries/verification/useVerificationsRemoveMutation.tsx
@@ -34,9 +34,7 @@ export function useVerificationsRemoveMutation() {
const qc = useQueryClient()
const deerVerificationEnabled = useDeerVerificationEnabled()
- const deerVerificationTrusted = useDeerVerificationTrusted(
- currentAccount?.did,
- )
+ const deerVerificationTrusted = useDeerVerificationTrusted()
const constellationInstance = useConstellationInstance()
return useMutation({
@@ -102,7 +100,7 @@ export function useVerificationsRemoveMutation() {
async onSuccess(_, {profile}) {
ax.metric('verification:revoke', {})
await updateProfileVerificationCache({profile})
- qc.invalidateQueries({
+ await qc.invalidateQueries({
queryKey: DEER_VERIFICATION_RQKEY(profile.did, deerVerificationTrusted),
})
},