134 lines
4.5 KiB
Diff
134 lines
4.5 KiB
Diff
--- 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,
|
|
@@ -9,9 +9,11 @@
|
|
} from '@atproto/api'
|
|
import {msg, Trans} from '@lingui/macro'
|
|
import {useLingui} from '@lingui/react'
|
|
+import {useQuery} from '@tanstack/react-query'
|
|
|
|
import {useActorStatus} from '#/lib/actor-status'
|
|
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'
|
|
@@ -45,6 +47,103 @@
|
|
import {ProfileHeaderMetrics} from './Metrics'
|
|
import {ProfileHeaderShell} from './Shell'
|
|
import {ProfileHeaderSuggestedFollows} from './SuggestedFollows'
|
|
+
|
|
+const SERVICE_FAVICONS: Record<string, any> = {
|
|
+ '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<string> {
|
|
+ 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<string>()
|
|
+ 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 (
|
|
+ <View style={[a.flex_row, a.flex_wrap, a.gap_sm, a.pt_xs]}>
|
|
+ {services.map(service => (
|
|
+ <Pressable
|
|
+ key={service}
|
|
+ onPress={() =>
|
|
+ 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},
|
|
+ ]}>
|
|
+ <Image
|
|
+ source={
|
|
+ SERVICE_FAVICONS[service] || {
|
|
+ uri: `https://www.google.com/s2/favicons?domain=${service}&sz=32`,
|
|
+ }
|
|
+ }
|
|
+ style={{width: 14, height: 14, borderRadius: 3}}
|
|
+ accessibilityIgnoresInvertColors
|
|
+ />
|
|
+ <Text
|
|
+ style={[
|
|
+ a.text_xs,
|
|
+ a.font_medium,
|
|
+ t.atoms.text_contrast_medium,
|
|
+ ]}>
|
|
+ {service}
|
|
+ </Text>
|
|
+ </Pressable>
|
|
+ ))}
|
|
+ </View>
|
|
+ )
|
|
+}
|
|
|
|
interface Props {
|
|
profile: AppBskyActorDefs.ProfileViewDetailed
|
|
@@ -151,6 +250,7 @@
|
|
{!isPlaceholderProfile && !isBlockedUser && (
|
|
<View style={a.gap_md}>
|
|
<ProfileHeaderMetrics profile={profile} />
|
|
+ <ProfileServiceLinks profile={profile} />
|
|
{descriptionRT && !moderation.ui('profileView').blur ? (
|
|
<View pointerEvents="auto">
|
|
<RichText
|