--- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -1,5 +1,5 @@ import {memo, useMemo, useState} from 'react' -import {View} from 'react-native' +import {Image, Pressable, View} from 'react-native' import { type AppBskyActorDefs, moderateProfile, @@ -11,7 +11,10 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' +import {useQuery} from '@tanstack/react-query' + import {useHaptics} from '#/lib/haptics' +import {useOpenLink} from '#/lib/hooks/useOpenLink' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' @@ -47,6 +50,103 @@ import {ProfileHeaderShell} from './Shell' import {ProfileHeaderSuggestedFollows} from './SuggestedFollows' +const SERVICE_FAVICONS: Record = { + 'syui.ai': require('../../../../assets/favicons/syui.ai.png'), + 'bsky.app': require('../../../../assets/favicons/bsky.app.png'), + 'atproto.com': require('../../../../assets/favicons/atproto.com.png'), +} + +async function resolvePds(did: string): Promise { + if (did.startsWith('did:web:')) { + const host = did.split(':').slice(2).join(':') + const res = await fetch(`https://${host}/.well-known/did.json`) + if (!res.ok) throw new Error('failed to resolve did:web') + const doc = await res.json() + const pds = doc.service?.find((s: any) => s.id === '#atproto_pds')?.serviceEndpoint + if (pds) return pds + return `https://${host}` + } + const res = await fetch(`https://plc.directory/${did}`) + if (!res.ok) throw new Error('failed to resolve DID') + const doc = await res.json() + const pds = doc.service?.find((s: any) => s.id === '#atproto_pds')?.serviceEndpoint + if (!pds) throw new Error('no PDS found') + return pds +} + +function ProfileServiceLinks({ + profile, +}: { + profile: AppBskyActorDefs.ProfileViewDetailed +}) { + const t = useTheme() + const openLink = useOpenLink() + + const {data: services} = useQuery({ + queryKey: ['profile-services', profile.did], + queryFn: async () => { + const pds = await resolvePds(profile.did) + const res = await fetch( + `${pds}/xrpc/com.atproto.repo.describeRepo?repo=${encodeURIComponent(profile.did)}`, + ) + if (!res.ok) throw new Error('failed') + const data = await res.json() + const collections: string[] = data.collections || [] + const serviceSet = new Set() + for (const nsid of collections) { + const parts = nsid.split('.') + if (parts.length >= 2) { + const domain = parts.slice(0, 2).reverse().join('.') + serviceSet.add(domain) + } + } + return Array.from(serviceSet) + }, + }) + + if (!services || services.length === 0) return null + + return ( + + {services.map(service => ( + + openLink( + `https://at.syu.is/@${profile.handle}/at/service/${service}`, + ) + } + style={[ + a.flex_row, + a.align_center, + a.gap_xs, + a.rounded_full, + t.atoms.bg_contrast_50, + {paddingVertical: 6, paddingHorizontal: 10}, + ]}> + + + {service} + + + ))} + + ) +} + interface Props { profile: AppBskyActorDefs.ProfileViewDetailed descriptionRT: RichTextAPI | null @@ -152,6 +252,7 @@ {!isPlaceholderProfile && !isBlockedUser && ( + {descriptionRT && !moderation.ui('profileView').blur ? (