Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: new trusted verifiers flow

- toggle trusting verifiers set by/from the appview
- make it obvious that you trust yourself
- let you quickly mark verifiers as trusted in the settings screen, instead of having to go to their profile

+463 -118
+4 -1
src/components/dialogs/SearchablePeopleList.tsx
··· 77 77 title, 78 78 showRecentConvos, 79 79 sortByMessageDeclaration, 80 + excludeSelf = true, 80 81 onSelectChat, 81 82 renderProfileCard, 82 83 }: { 83 84 title: string 84 85 showRecentConvos?: boolean 85 86 sortByMessageDeclaration?: boolean 87 + excludeSelf?: boolean 86 88 } & ( 87 89 | { 88 90 renderProfileCard: (item: ProfileItem) => React.ReactNode ··· 131 133 } else if (searchText.length) { 132 134 if (results?.length) { 133 135 for (const profile of results) { 134 - if (profile.did === currentAccount?.did) continue 136 + if (excludeSelf && profile.did === currentAccount?.did) continue 135 137 _items.push({ 136 138 type: 'profile', 137 139 key: profile.did, ··· 242 244 results, 243 245 isError, 244 246 currentAccount?.did, 247 + excludeSelf, 245 248 follows, 246 249 convos, 247 250 showRecentConvos,
+354 -63
src/screens/Settings/RunesSettings/BadgesSettings.tsx
··· 1 - import {useState} from 'react' 2 - import {View} from 'react-native' 3 - import {type ProfileViewBasic} from '@atproto/api/dist/client/types/app/bsky/actor/defs' 4 - import {Trans, useLingui} from '@lingui/react/macro' 1 + import {useCallback, useMemo, useState} from 'react' 2 + import {LayoutAnimation, View} from 'react-native' 3 + import {useReducedMotion} from 'react-native-reanimated' 4 + import {type ModerationOpts} from '@atproto/api' 5 + import {Plural, Trans, useLingui} from '@lingui/react/macro' 5 6 6 7 import {usePalette} from '#/lib/hooks/usePalette' 7 8 import * as persisted from '#/state/persisted' 8 9 import { 10 + useDeerVerification, 9 11 useDeerVerificationEnabled, 12 + useDeerVerificationTrustAppView, 10 13 useDeerVerificationTrusted, 14 + useSetDeerVerification, 11 15 useSetDeerVerificationEnabled, 16 + useSetDeerVerificationTrust, 12 17 } from '#/state/preferences/deer-verification' 18 + import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 13 19 import { 14 20 useFaviconService, 15 21 useSetFaviconService, ··· 22 28 useSetPdsLabelHideBskyPds, 23 29 } from '#/state/preferences/pds-label' 24 30 import {useProfilesQuery} from '#/state/queries/profile' 31 + import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' 32 + import {useSession} from '#/state/session' 25 33 import * as SettingsList from '#/screens/Settings/components/SettingsList' 26 - import {atoms as a, useBreakpoints} from '#/alf' 34 + import {atoms as a, useTheme} from '#/alf' 27 35 import {Admonition} from '#/components/Admonition' 28 - import {Button, ButtonText} from '#/components/Button' 36 + import {Button, ButtonIcon, ButtonText} from '#/components/Button' 29 37 import * as Dialog from '#/components/Dialog' 38 + import { 39 + type ProfileItem, 40 + SearchablePeopleList, 41 + } from '#/components/dialogs/SearchablePeopleList' 30 42 import * as Toggle from '#/components/forms/Toggle' 43 + import { 44 + ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon, 45 + ChevronTop_Stroke2_Corner0_Rounded as ChevronTopIcon, 46 + } from '#/components/icons/Chevron' 31 47 import {PaintRoller_Stroke2_Corner2_Rounded as PaintRollerIcon} from '#/components/icons/PaintRoller' 48 + import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus' 32 49 import {Star_Stroke2_Corner0_Rounded as StarIcon} from '#/components/icons/Star' 50 + import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' 33 51 import {Verified_Stroke2_Corner2_Rounded as VerifiedIcon} from '#/components/icons/Verified' 52 + import * as ProfileCard from '#/components/ProfileCard' 53 + import * as Prompt from '#/components/Prompt' 54 + import * as Toast from '#/components/Toast' 34 55 import {Text} from '#/components/Typography' 35 56 import {IS_WEB} from '#/env' 36 - import {SearchProfileCard} from '../../Search/components/SearchProfileCard' 57 + import type * as bsky from '#/types/bsky' 37 58 import {RunesScreenLayout} from './components/RunesScreenLayout' 38 59 39 60 export function RunesBadgesSettingsScreen() { 40 61 const {t: l} = useLingui() 41 62 63 + const deerVerification = useDeerVerification() 42 64 const deerVerificationEnabled = useDeerVerificationEnabled() 65 + const trustAppView = useDeerVerificationTrustAppView() 66 + const setDeerVerification = useSetDeerVerification() 43 67 const setDeerVerificationEnabled = useSetDeerVerificationEnabled() 44 - const setTrustedVerifiersDialogControl = Dialog.useDialogControl() 45 68 46 69 const pdsLabelEnabled = usePdsLabelEnabled() 47 70 const setPdsLabelEnabled = useSetPdsLabelEnabled() ··· 51 74 52 75 return ( 53 76 <RunesScreenLayout titleText={l`Badges`}> 54 - <Toggle.Item 55 - name="custom_verifications" 56 - label={l`Select trusted verifiers and act as one`} 57 - value={deerVerificationEnabled} 58 - onChange={value => setDeerVerificationEnabled(value)}> 59 - <SettingsList.Item> 60 - <SettingsList.ItemIcon icon={VerifiedIcon} /> 61 - <SettingsList.ItemText> 62 - <Trans>Select trusted verifiers and act as one</Trans> 63 - </SettingsList.ItemText> 77 + <SettingsList.Group contentContainerStyle={[a.gap_sm]}> 78 + <SettingsList.ItemIcon icon={VerifiedIcon} /> 79 + <SettingsList.ItemText> 80 + <Trans>Trusted verifiers</Trans> 81 + </SettingsList.ItemText> 82 + <Toggle.Item 83 + name="custom_verifications" 84 + label={l`Use own selection of trusted verifiers`} 85 + value={deerVerificationEnabled} 86 + onChange={value => setDeerVerificationEnabled(value)} 87 + style={[a.w_full]}> 88 + <Toggle.LabelText style={[a.flex_1]}> 89 + <Trans>Use own selection of trusted verifiers</Trans> 90 + </Toggle.LabelText> 64 91 <Toggle.Platform /> 65 - </SettingsList.Item> 66 - </Toggle.Item> 92 + </Toggle.Item> 93 + {deerVerificationEnabled && ( 94 + <Toggle.Item 95 + name="trust_verifiers_from_appview" 96 + label={l`Trust verifiers from current AppView`} 97 + value={trustAppView} 98 + onChange={value => 99 + setDeerVerification({...deerVerification, trustAppView: value}) 100 + } 101 + style={[a.w_full]}> 102 + <Toggle.LabelText style={[a.flex_1]}> 103 + <Trans>Trust verifiers from current AppView</Trans> 104 + </Toggle.LabelText> 105 + <Toggle.Platform /> 106 + </Toggle.Item> 107 + )} 108 + </SettingsList.Group> 67 109 <SettingsList.Item> 68 110 <Admonition type="warning" style={[a.flex_1]}> 69 111 <Trans> ··· 76 118 </Trans> 77 119 </Admonition> 78 120 </SettingsList.Item> 79 - <SettingsList.Item> 80 - <SettingsList.ItemIcon icon={VerifiedIcon} /> 81 - <SettingsList.ItemText> 82 - <Trans>{`Trusted Verifiers`}</Trans> 83 - </SettingsList.ItemText> 84 - <SettingsList.BadgeButton 85 - label={l`View`} 86 - onPress={() => setTrustedVerifiersDialogControl.open()} 87 - /> 88 - </SettingsList.Item> 121 + <TrustedVerifiersSection /> 89 122 90 123 <SettingsList.Divider /> 91 124 ··· 133 166 )} 134 167 135 168 <FaviconServiceDialog control={setFaviconServiceControl} /> 136 - <TrustedVerifiersDialog control={setTrustedVerifiersDialogControl} /> 137 169 </RunesScreenLayout> 138 170 ) 139 171 } ··· 231 263 ) 232 264 } 233 265 234 - function TrustedVerifiersDialog({ 235 - control, 266 + function TrustedVerifiersSection() { 267 + const {t: l} = useLingui() 268 + const reducedMotion = useReducedMotion() 269 + const addTrustedVerifierDialogControl = Dialog.useDialogControl() 270 + const resetTrustedVerifiersPromptControl = Prompt.usePromptControl() 271 + const deerVerification = useDeerVerification() 272 + const setDeerVerification = useSetDeerVerification() 273 + const currentAccountProfile = useCurrentAccountProfile() 274 + const {currentAccount} = useSession() 275 + const currentAccountDid = currentAccount?.did 276 + const [isExpanded, setIsExpanded] = useState(false) 277 + const trusted = useDeerVerificationTrusted() 278 + 279 + const results = useProfilesQuery({ 280 + handles: Array.from(trusted).filter( 281 + did => did !== currentAccountProfile?.did, 282 + ), 283 + maintainData: true, 284 + }) 285 + 286 + const trustedProfiles = useMemo(() => { 287 + const profiles: bsky.profile.AnyProfileView[] = [] 288 + const seen = new Set<string>() 289 + 290 + if (currentAccountProfile && trusted.has(currentAccountProfile.did)) { 291 + profiles.push(currentAccountProfile) 292 + seen.add(currentAccountProfile.did) 293 + } 294 + 295 + for (const profile of results.data?.profiles ?? []) { 296 + if (!trusted.has(profile.did)) continue 297 + if (seen.has(profile.did)) continue 298 + profiles.push(profile) 299 + seen.add(profile.did) 300 + } 301 + 302 + return profiles 303 + }, [currentAccountProfile, results.data?.profiles, trusted]) 304 + 305 + const onToggleExpanded = useCallback(() => { 306 + if (!reducedMotion) { 307 + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 308 + } 309 + setIsExpanded(current => !current) 310 + }, [reducedMotion]) 311 + 312 + const onResetAll = useCallback(() => { 313 + setDeerVerification({ 314 + ...deerVerification, 315 + trustedSelf: true, 316 + trusted: [], 317 + }) 318 + }, [deerVerification, setDeerVerification]) 319 + 320 + return ( 321 + <> 322 + <SettingsList.PressableItem 323 + label={l`Trusted verifiers`} 324 + accessibilityHint={l`Shows the trusted verifiers you act as`} 325 + onPress={onToggleExpanded}> 326 + <SettingsList.ItemIcon icon={VerifiedIcon} /> 327 + <SettingsList.ItemText> 328 + <Trans>{`Trusted Verifiers`}</Trans> 329 + </SettingsList.ItemText> 330 + {!isExpanded && ( 331 + <SettingsList.BadgeText> 332 + <Plural value={trusted.size} one="# selected" other="# selected" /> 333 + </SettingsList.BadgeText> 334 + )} 335 + <SettingsList.ItemIcon 336 + icon={isExpanded ? ChevronTopIcon : ChevronBottomIcon} 337 + size="md" 338 + /> 339 + </SettingsList.PressableItem> 340 + 341 + {isExpanded && ( 342 + <View style={[a.pb_sm]}> 343 + {trustedProfiles.length > 0 ? ( 344 + trustedProfiles.map(profile => ( 345 + <TrustedVerifierRow 346 + key={profile.did} 347 + profile={profile} 348 + isCurrentAccount={profile.did === currentAccountProfile?.did} 349 + /> 350 + )) 351 + ) : ( 352 + <SettingsList.Item iconInset> 353 + <Text style={[a.text_sm, a.text_center, a.flex_1]}> 354 + <Trans>No trusted verifiers selected.</Trans> 355 + </Text> 356 + </SettingsList.Item> 357 + )} 358 + 359 + <View style={[a.px_xl, a.pt_sm, a.gap_sm, a.flex_row]}> 360 + <Button 361 + label={l`Reset all trusted verifiers`} 362 + size="small" 363 + color="negative_subtle" 364 + style={[a.flex_1]} 365 + disabled={!currentAccountDid} 366 + onPress={() => resetTrustedVerifiersPromptControl.open()}> 367 + <ButtonText> 368 + <Trans>Reset all</Trans> 369 + </ButtonText> 370 + </Button> 371 + <Button 372 + label={l`Add trusted verifiers`} 373 + size="small" 374 + color="secondary" 375 + style={[a.flex_1]} 376 + onPress={() => addTrustedVerifierDialogControl.open()}> 377 + <ButtonIcon icon={PlusIcon} /> 378 + <ButtonText> 379 + <Trans>Add trusted verifiers</Trans> 380 + </ButtonText> 381 + </Button> 382 + </View> 383 + </View> 384 + )} 385 + 386 + <TrustedVerifiersAddDialog control={addTrustedVerifierDialogControl} /> 387 + <Prompt.Basic 388 + control={resetTrustedVerifiersPromptControl} 389 + title={l`Are you sure?`} 390 + description={l`This will clear your selected trusted verifiers and trust your current account again.`} 391 + confirmButtonCta={l`Reset all`} 392 + confirmButtonColor="negative" 393 + onConfirm={onResetAll} 394 + /> 395 + </> 396 + ) 397 + } 398 + 399 + function TrustedVerifierRow({ 400 + profile, 401 + isCurrentAccount, 236 402 }: { 237 - control: Dialog.DialogControlProps 403 + profile: bsky.profile.AnyProfileView 404 + isCurrentAccount: boolean 238 405 }) { 239 406 const {t: l} = useLingui() 407 + const moderationOpts = useModerationOpts() 408 + const enableSquareButtons = useEnableSquareButtons() 409 + const setDeerVerificationTrust = useSetDeerVerificationTrust() 410 + const t = useTheme() 240 411 241 - return ( 242 - <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> 243 - <Dialog.Handle /> 244 - <Dialog.ScrollableInner label={l`Trusted Verifiers`}> 245 - <View style={[a.gap_sm, a.pb_lg]}> 246 - <Text style={[a.text_2xl, a.font_bold]}> 247 - <Trans>Trusted Verifiers</Trans> 248 - </Text> 249 - </View> 412 + const onRemove = useCallback(() => { 413 + setDeerVerificationTrust.remove(profile.did) 414 + Toast.show(l`Removed trusted verifier`) 415 + }, [l, profile.did, setDeerVerificationTrust]) 250 416 251 - <TrustedVerifiers /> 417 + if (!moderationOpts) return null 252 418 253 - <Dialog.Close /> 254 - </Dialog.ScrollableInner> 419 + return ( 420 + <View style={[a.px_xl, a.py_sm]}> 421 + <ProfileCard.Header> 422 + <ProfileCard.Link profile={profile} style={[a.flex_1]}> 423 + <View style={[a.flex_1, a.w_full]}> 424 + <ProfileCard.Header> 425 + <ProfileCard.Avatar 426 + profile={profile} 427 + moderationOpts={moderationOpts} 428 + disabledPreview 429 + /> 430 + <View style={[a.flex_1]}> 431 + <ProfileCard.NameAndHandle 432 + profile={profile} 433 + moderationOpts={moderationOpts} 434 + /> 435 + {isCurrentAccount && ( 436 + <Text 437 + style={[a.text_xs, t.atoms.text_contrast_low, a.pt_2xs]}> 438 + <Trans>Current account</Trans> 439 + </Text> 440 + )} 441 + </View> 442 + </ProfileCard.Header> 443 + </View> 444 + </ProfileCard.Link> 445 + <Button 446 + label={ 447 + isCurrentAccount 448 + ? l`Remove current account from trusted verifiers` 449 + : l`Remove trusted verifier` 450 + } 451 + color="secondary" 452 + variant="ghost" 453 + size="small" 454 + shape={enableSquareButtons ? 'square' : 'round'} 455 + onPress={onRemove}> 456 + <ButtonIcon icon={XIcon} size="md" /> 457 + </Button> 458 + </ProfileCard.Header> 459 + </View> 460 + ) 461 + } 462 + 463 + function TrustedVerifiersAddDialog({ 464 + control, 465 + }: { 466 + control: Dialog.DialogControlProps 467 + }) { 468 + return ( 469 + <Dialog.Outer control={control} nativeOptions={{fullHeight: true}}> 470 + <Dialog.Handle /> 471 + <TrustedVerifiersAddDialogInner /> 255 472 </Dialog.Outer> 256 473 ) 257 474 } 258 475 259 - function TrustedVerifiers() { 260 - const trusted = useDeerVerificationTrusted() 476 + function TrustedVerifiersAddDialogInner() { 477 + const {t: l} = useLingui() 261 478 const moderationOpts = useModerationOpts() 262 - const {gtMobile} = useBreakpoints() 479 + const trusted = useDeerVerificationTrusted() 480 + const setDeerVerificationTrust = useSetDeerVerificationTrust() 481 + const {currentAccount} = useSession() 263 482 264 - const results = useProfilesQuery({ 265 - handles: Array.from(trusted), 266 - }) 267 - 268 - if (!results.data || moderationOpts === undefined) { 269 - return null 270 - } 483 + const renderProfileCard = useCallback( 484 + (item: ProfileItem) => { 485 + if (!moderationOpts) return null 271 486 272 - return ( 273 - <View style={[gtMobile ? a.pl_md : a.pl_sm, a.pb_sm]}> 274 - {results.data.profiles.map(profile => ( 275 - <SearchProfileCard 276 - key={profile.did} 277 - profile={profile as ProfileViewBasic} 487 + return ( 488 + <TrustedVerifierSearchResult 489 + profile={item.profile} 278 490 moderationOpts={moderationOpts} 491 + isTrusted={trusted.has(item.profile.did)} 492 + isCurrentAccount={item.profile.did === currentAccount?.did} 493 + onToggle={() => { 494 + if (trusted.has(item.profile.did)) { 495 + setDeerVerificationTrust.remove(item.profile.did) 496 + Toast.show(l`Removed trusted verifier`) 497 + } else { 498 + setDeerVerificationTrust.add(item.profile.did) 499 + Toast.show(l`Added trusted verifier`) 500 + } 501 + }} 279 502 /> 280 - ))} 503 + ) 504 + }, 505 + [currentAccount?.did, l, moderationOpts, setDeerVerificationTrust, trusted], 506 + ) 507 + 508 + return ( 509 + <SearchablePeopleList 510 + title={l`Add trusted verifiers`} 511 + renderProfileCard={renderProfileCard} 512 + excludeSelf={false} 513 + /> 514 + ) 515 + } 516 + 517 + function TrustedVerifierSearchResult({ 518 + profile, 519 + moderationOpts, 520 + isTrusted, 521 + isCurrentAccount, 522 + onToggle, 523 + }: { 524 + profile: bsky.profile.AnyProfileView 525 + moderationOpts: ModerationOpts 526 + isTrusted: boolean 527 + isCurrentAccount: boolean 528 + onToggle: () => void 529 + }) { 530 + const {t: l} = useLingui() 531 + const t = useTheme() 532 + 533 + return ( 534 + <View style={[a.flex_1, a.py_sm, a.px_lg]}> 535 + <ProfileCard.Header> 536 + <ProfileCard.Link profile={profile} style={[a.flex_1]}> 537 + <View style={[a.flex_1, a.w_full]}> 538 + <ProfileCard.Header> 539 + <ProfileCard.Avatar 540 + profile={profile} 541 + moderationOpts={moderationOpts} 542 + disabledPreview 543 + /> 544 + <View style={[a.flex_1]}> 545 + <ProfileCard.NameAndHandle 546 + profile={profile} 547 + moderationOpts={moderationOpts} 548 + /> 549 + {isCurrentAccount && ( 550 + <Text 551 + style={[a.text_xs, t.atoms.text_contrast_low, a.pt_2xs]}> 552 + <Trans>Current account</Trans> 553 + </Text> 554 + )} 555 + </View> 556 + </ProfileCard.Header> 557 + </View> 558 + </ProfileCard.Link> 559 + <Button 560 + label={ 561 + isTrusted ? l`Remove trusted verifier` : l`Add trusted verifier` 562 + } 563 + onPress={onToggle} 564 + size="small" 565 + variant="solid" 566 + color="secondary"> 567 + <ButtonText> 568 + {isTrusted ? <Trans>Remove</Trans> : <Trans>Add</Trans>} 569 + </ButtonText> 570 + </Button> 571 + </ProfileCard.Header> 281 572 </View> 282 573 ) 283 574 }
+5 -24
src/state/persisted/schema.ts
··· 200 200 deerVerification: z 201 201 .object({ 202 202 enabled: z.boolean(), 203 + trustAppView: z.boolean().optional(), 204 + trustedSelf: z.boolean().optional(), 203 205 trusted: z.array(z.string()), 204 206 }) 205 207 .optional(), ··· 330 332 showViaClient: true, 331 333 deerVerification: { 332 334 enabled: false, 333 - // https://witchsky.app/profile/did:plc:p2cp5gopk7mgjegy6wadk3ep/post/3lndyqyyr4k2k 334 - // using https://bverified.vercel.app/trusted as a source 335 - trusted: [ 336 - 'did:plc:z72i7hdynmk6r22z27h6tvur', 337 - 'did:plc:b2kutgxqlltwc6lhs724cfwr', 338 - 'did:plc:inz4fkbbp7ms3ixufw6xuvdi', 339 - 'did:plc:eclio37ymobqex2ncko63h4r', 340 - 'did:plc:dzezcmpb3fhcpns4n4xm4ur5', 341 - 'did:plc:5u54z2qgkq43dh2nzwzdbbhb', 342 - 'did:plc:wmho6q2uiyktkam3jsvrms3s', 343 - 'did:plc:sqbswn3lalcc2dlh2k7zdpuw', 344 - 'did:plc:k5nskatzhyxersjilvtnz4lh', 345 - 'did:plc:d2jith367s6ybc3ldsusgdae', 346 - 'did:plc:y3xrmnwvkvsq4tqcsgwch4na', 347 - 'did:plc:i3fhjvvkbmirhyu4aeihhrnv', 348 - 'did:plc:fivojrvylkim4nuo3pfqcf3k', 349 - 'did:plc:ofbkqcjzvm6gtwuufsubnkaf', 350 - 'did:plc:xwqgusybtrpm67tcwqdfmzvy', 351 - 'did:plc:oxo226vi7t2btjokm2buusoy', 352 - 'did:plc:r4ve5hjtfjubdwrvlxcad62e', 353 - 'did:plc:j4eroku3volozvv6ljsnnfec', 354 - 'did:plc:6q2thhy2ohzog26mmqm4pffk', 355 - 'did:plc:rk25gdgk3cnnmtkvlae265nz', 356 - ], 335 + trustAppView: true, 336 + trustedSelf: true, 337 + trusted: [], 357 338 }, 358 339 highQualityImages: false, 359 340 imageCdnHost: 'https://cdn.bsky.app',
+52 -12
src/state/preferences/deer-verification.tsx
··· 9 9 } from 'react' 10 10 11 11 import * as persisted from '#/state/persisted' 12 + import {useSession} from '#/state/session' 12 13 13 14 type StateContext = persisted.Schema['deerVerification'] 14 15 type SetContext = (v: persisted.Schema['deerVerification']) => void ··· 26 27 const setStateWrapped = useCallback( 27 28 (deerVerification: persisted.Schema['deerVerification']) => { 28 29 setState(deerVerification) 29 - persisted.write('deerVerification', deerVerification) 30 + void persisted.write('deerVerification', deerVerification) 30 31 }, 31 32 [setState], 32 33 ) ··· 54 55 return useDeerVerification().enabled 55 56 } 56 57 57 - export function useDeerVerificationTrusted( 58 - mandatory: string | undefined = undefined, 59 - ) { 60 - const trusted = new Set(useDeerVerification().trusted) 61 - if (mandatory) { 62 - trusted.add(mandatory) 63 - } 64 - return trusted 58 + export function useDeerVerificationTrustAppView() { 59 + return useDeerVerification().trustAppView ?? true 60 + } 61 + 62 + export function useDeerVerificationTrusted() { 63 + const deerVerification = useDeerVerification() 64 + const {currentAccount} = useSession() 65 + const currentAccountDid = currentAccount?.did 66 + const trustedSelf = deerVerification.trustedSelf ?? true 67 + 68 + return useMemo(() => { 69 + const trusted = new Set(deerVerification.trusted) 70 + if (trustedSelf && currentAccountDid) { 71 + trusted.add(currentAccountDid) 72 + } 73 + return trusted 74 + }, [currentAccountDid, deerVerification.trusted, trustedSelf]) 65 75 } 66 76 67 77 export function useSetDeerVerification() { ··· 82 92 export function useSetDeerVerificationTrust() { 83 93 const deerVerification = useDeerVerification() 84 94 const setDeerVerification = useSetDeerVerification() 95 + const {currentAccount} = useSession() 96 + const currentAccountDid = currentAccount?.did 85 97 86 98 return useMemo( 87 99 () => ({ 88 100 add: (add: string) => { 89 101 const trusted = new Set(deerVerification.trusted) 102 + if (add === currentAccountDid) { 103 + trusted.delete(add) 104 + setDeerVerification({ 105 + ...deerVerification, 106 + trustedSelf: true, 107 + trusted: Array.from(trusted), 108 + }) 109 + return 110 + } 111 + 90 112 trusted.add(add) 91 - setDeerVerification({...deerVerification, trusted: Array.from(trusted)}) 113 + setDeerVerification({ 114 + ...deerVerification, 115 + trustedSelf: deerVerification.trustedSelf ?? true, 116 + trusted: Array.from(trusted), 117 + }) 92 118 }, 93 119 remove: (remove: string) => { 94 120 const trusted = new Set(deerVerification.trusted) 121 + if (remove === currentAccountDid) { 122 + trusted.delete(remove) 123 + setDeerVerification({ 124 + ...deerVerification, 125 + trustedSelf: false, 126 + trusted: Array.from(trusted), 127 + }) 128 + return 129 + } 130 + 95 131 trusted.delete(remove) 96 - setDeerVerification({...deerVerification, trusted: Array.from(trusted)}) 132 + setDeerVerification({ 133 + ...deerVerification, 134 + trustedSelf: deerVerification.trustedSelf ?? true, 135 + trusted: Array.from(trusted), 136 + }) 97 137 }, 98 138 }), 99 - [deerVerification, setDeerVerification], 139 + [currentAccountDid, deerVerification, setDeerVerification], 100 140 ) 101 141 }
+44 -10
src/state/queries/deer-verification.ts
··· 11 11 import {useConstellationInstance} from '../preferences/constellation-instance' 12 12 import { 13 13 useDeerVerificationEnabled, 14 + useDeerVerificationTrustAppView, 14 15 useDeerVerificationTrusted, 15 16 } from '../preferences/deer-verification' 16 17 import { ··· 24 25 } from './constellation' 25 26 import {LRU} from './direct-fetch-record' 26 27 import {resolvePdsServiceUrl} from './resolve-identity' 27 - import {useCurrentAccountProfile} from './useCurrentAccountProfile' 28 28 29 29 const RQKEY_ROOT = 'deer-verification' 30 30 export const RQKEY = (did: string, trusted: Set<string>) => [ ··· 53 53 from_dids: Array.from(trusted), 54 54 }) 55 55 return asyncGenDedupe( 56 - asyncGenFilter(verificationLinks, (link) => trusted.has(link.did)), 57 - (link) => link.did, 56 + asyncGenFilter(verificationLinks, link => trusted.has(link.did)), 57 + link => link.did, 58 58 ) 59 59 } 60 60 ··· 84 84 const record = await verificationCache.getOrTryInsertWith( 85 85 request, 86 86 async () => { 87 - const resp = await (await fetch(request)).json() 87 + const resp = (await (await fetch(request)).json()) as { 88 + value?: unknown 89 + } 88 90 return resp.value 89 91 }, 90 92 ) ··· 122 124 createdAt: record.createdAt, 123 125 uri: asUri(link), 124 126 })) 127 + } 128 + 129 + function mergeVerificationViews( 130 + appViewVerifications: VerificationView[], 131 + deerVerifications: VerificationView[], 132 + ) { 133 + const merged = new Map<string, VerificationView>() 134 + 135 + for (const verification of appViewVerifications) { 136 + merged.set(verification.uri, verification) 137 + } 138 + 139 + for (const verification of deerVerifications) { 140 + merged.set(verification.uri, verification) 141 + } 142 + 143 + return Array.from(merged.values()) 125 144 } 126 145 127 146 function createVerificationState( 128 - verifications: VerificationView[], 147 + appViewVerifications: VerificationView[], 148 + deerVerifications: VerificationView[], 129 149 profile: AnyProfileView, 130 150 trusted: Set<string>, 151 + trustAppView: boolean, 131 152 ): VerificationState { 153 + const verifications = mergeVerificationViews( 154 + appViewVerifications, 155 + deerVerifications, 156 + ) 157 + const appViewTrustedVerifierStatus = trustAppView 158 + ? profile.verification?.trustedVerifierStatus 159 + : undefined 160 + const trustedVerifierStatus = trusted.has(profile.did) 161 + ? 'valid' 162 + : (appViewTrustedVerifierStatus ?? 'none') 163 + 132 164 return { 133 165 verifications, 134 166 verifiedStatus: ··· 137 169 ? 'valid' 138 170 : 'invalid' 139 171 : 'none', 140 - trustedVerifierStatus: trusted.has(profile.did) ? 'valid' : 'none', 172 + trustedVerifierStatus, 141 173 } 142 174 } 143 175 ··· 149 181 enabled?: boolean 150 182 }) { 151 183 const instance = useConstellationInstance() 152 - const currentAccountProfile = useCurrentAccountProfile() 153 - const trusted = useDeerVerificationTrusted(currentAccountProfile?.did) 184 + const trusted = useDeerVerificationTrusted() 185 + const trustAppView = useDeerVerificationTrustAppView() 154 186 155 187 const linkedRecords = useQuery<LinkedRecord[] | undefined>({ 156 188 staleTime: STALE.HOURS.ONE, ··· 168 200 }) 169 201 170 202 if (linkedRecords.data === undefined || profile === undefined) return 171 - const verifications = createVerificationViews(linkedRecords.data, profile) 203 + const deerVerifications = createVerificationViews(linkedRecords.data, profile) 172 204 const verificationState = createVerificationState( 173 - verifications, 205 + trustAppView ? (profile.verification?.verifications ?? []) : [], 206 + deerVerifications, 174 207 profile, 175 208 trusted, 209 + trustAppView, 176 210 ) 177 211 178 212 return verificationState
+2 -4
src/state/queries/verification/useVerificationCreateMutation.tsx
··· 25 25 26 26 const qc = useQueryClient() 27 27 const deerVerificationEnabled = useDeerVerificationEnabled() 28 - const deerVerificationTrusted = useDeerVerificationTrusted( 29 - currentAccount?.did, 30 - ) 28 + const deerVerificationTrusted = useDeerVerificationTrusted() 31 29 const constellationInstance = useConstellationInstance() 32 30 33 31 return useMutation({ ··· 86 84 async onSuccess(_, {profile}) { 87 85 ax.metric('verification:create', {}) 88 86 await updateProfileVerificationCache({profile}) 89 - qc.invalidateQueries({ 87 + await qc.invalidateQueries({ 90 88 queryKey: DEER_VERIFICATION_RQKEY(profile.did, deerVerificationTrusted), 91 89 }) 92 90 },
+2 -4
src/state/queries/verification/useVerificationsRemoveMutation.tsx
··· 34 34 35 35 const qc = useQueryClient() 36 36 const deerVerificationEnabled = useDeerVerificationEnabled() 37 - const deerVerificationTrusted = useDeerVerificationTrusted( 38 - currentAccount?.did, 39 - ) 37 + const deerVerificationTrusted = useDeerVerificationTrusted() 40 38 const constellationInstance = useConstellationInstance() 41 39 42 40 return useMutation({ ··· 102 100 async onSuccess(_, {profile}) { 103 101 ax.metric('verification:revoke', {}) 104 102 await updateProfileVerificationCache({profile}) 105 - qc.invalidateQueries({ 103 + await qc.invalidateQueries({ 106 104 queryKey: DEER_VERIFICATION_RQKEY(profile.did, deerVerificationTrusted), 107 105 }) 108 106 },