forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useWindowDimensions, View} from 'react-native'
2import {msg} from '@lingui/core/macro'
3import {useLingui} from '@lingui/react'
4
5import {useProfileShadow} from '#/state/cache/profile-shadow'
6import {
7 usePdsLabelEnabled,
8 usePdsLabelHideBskyPds,
9} from '#/state/preferences/pds-label'
10import {usePdsFaviconQuery, usePdsLabelQuery} from '#/state/queries/pds-label'
11import {atoms as a, useAlf, type ViewStyleProp} from '#/alf'
12import {BotBadge, BotBadgeButton, isBotAccount} from '#/components/BotBadge'
13import {Button} from '#/components/Button'
14import * as Dialog from '#/components/Dialog'
15import {PdsBadgeIcon, PdsDialog} from '#/components/PdsDialog'
16import {isPetAccount, PetBadge, PetBadgeButton} from '#/components/PetBadge'
17import {useSimpleVerificationState} from '#/components/verification'
18import {VerificationCheck} from '#/components/verification/VerificationCheck'
19import {VerificationCheckButton} from '#/components/verification/VerificationCheckButton'
20import {IS_WEB} from '#/env'
21import type * as bsky from '#/types/bsky'
22
23export type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
24
25const verificationIconSizes: Record<Size, number> = {
26 xs: 10,
27 sm: 12,
28 md: 14,
29 lg: 18,
30 xl: 22,
31} as const
32
33const botIconSizes: Record<Size, number> = {
34 xs: 11,
35 sm: 13,
36 md: 15,
37 lg: 19,
38 xl: 23,
39} as const
40
41export function ProfileBadges({
42 profile,
43 interactive = false,
44 pdsInteractive = true,
45 size,
46 style,
47}: ViewStyleProp & {
48 profile: bsky.profile.AnyProfileView
49 interactive?: boolean
50 pdsInteractive?: boolean
51 size: Size
52}) {
53 const shadowed = useProfileShadow(profile)
54 const verification = useSimpleVerificationState({profile})
55 const pdsLabelEnabled = usePdsLabelEnabled()
56 const hideBskyPds = usePdsLabelHideBskyPds()
57 const {data: pdsData, isLoading: isPdsLoading} = usePdsLabelQuery(
58 pdsLabelEnabled ? shadowed.did : undefined,
59 )
60 const {data: pdsFaviconUrl} = usePdsFaviconQuery(
61 pdsData && !pdsData.isBsky && !pdsData.isBridged
62 ? pdsData.pdsUrl
63 : undefined,
64 )
65 const {fontScale: nativeScaleMultiplier} = useWindowDimensions()
66 const {
67 fonts: {scaleMultiplier: alfScaleMultiplier},
68 } = useAlf()
69
70 const isBskyHandle =
71 !!shadowed.handle &&
72 (shadowed.handle.endsWith('.bsky.social') ||
73 shadowed.handle === 'bsky.social')
74
75 const showPdsBadge =
76 pdsLabelEnabled &&
77 (isPdsLoading || (!!pdsData && !(hideBskyPds && pdsData.isBsky)))
78
79 // if nothing to show, don't render the container at all
80 if (
81 !showPdsBadge &&
82 !verification.showBadge &&
83 !isBotAccount(shadowed) &&
84 !isPetAccount(shadowed)
85 )
86 return null
87
88 const isOnTheSmallSide = size === 'xs' || size === 'sm'
89
90 const verificationIconWidth =
91 verificationIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
92 const botIconWidth =
93 botIconSizes[size] * nativeScaleMultiplier * alfScaleMultiplier
94
95 return (
96 <View
97 style={[
98 a.flex_row,
99 a.align_center,
100 isOnTheSmallSide ? a.gap_2xs : a.gap_xs,
101 style,
102 ]}>
103 {showPdsBadge && (
104 <PdsInlineIcon
105 size={size}
106 interactive={pdsInteractive}
107 isLoading={isPdsLoading}
108 isBsky={pdsData?.isBsky ?? isBskyHandle}
109 isBridged={pdsData?.isBridged ?? false}
110 pdsUrl={pdsData?.pdsUrl}
111 faviconUrl={pdsFaviconUrl}
112 />
113 )}
114 {interactive ? (
115 <>
116 <VerificationCheckButton
117 profile={shadowed}
118 width={verificationIconWidth}
119 />
120 <BotBadgeButton profile={shadowed} width={botIconWidth} />
121 <PetBadgeButton profile={shadowed} width={botIconWidth} />
122 </>
123 ) : (
124 <>
125 {verification.showBadge && (
126 <VerificationCheck
127 verifier={verification.role === 'verifier'}
128 width={verificationIconWidth}
129 />
130 )}
131 <BotBadge profile={shadowed} width={botIconWidth} />
132 <PetBadge profile={shadowed} width={botIconWidth} />
133 </>
134 )}
135 </View>
136 )
137}
138
139function pdsIconDimensions(size: Size) {
140 switch (size) {
141 case 'md':
142 return 14
143 case 'lg':
144 return 20
145 case 'xl':
146 return 24
147 default:
148 return 12
149 }
150}
151
152function PdsInlineIcon({
153 size,
154 interactive,
155 isLoading,
156 isBsky,
157 isBridged,
158 pdsUrl,
159 faviconUrl,
160}: {
161 size: Size
162 interactive: boolean
163 isLoading: boolean
164 isBsky: boolean
165 isBridged: boolean
166 pdsUrl?: string
167 faviconUrl?: string
168}) {
169 const {_} = useLingui()
170 const dialogControl = Dialog.useDialogControl()
171 const dimensions = pdsIconDimensions(size)
172
173 const icon = (
174 <PdsBadgeIcon
175 faviconUrl={faviconUrl}
176 isBsky={isBsky}
177 isBridged={isBridged}
178 size={dimensions}
179 borderRadius={Math.round(dimensions * 0.25)}
180 />
181 )
182
183 if (isLoading || !pdsUrl || !interactive) {
184 return (
185 <View
186 style={[
187 a.justify_center,
188 a.align_center,
189 {width: dimensions, height: dimensions},
190 ]}>
191 {icon}
192 </View>
193 )
194 }
195
196 return (
197 <>
198 <Button
199 label={_(msg`View PDS information`)}
200 hitSlop={20}
201 onPress={evt => {
202 evt.preventDefault()
203 dialogControl.open()
204 if (IS_WEB) {
205 ;(document.activeElement as HTMLElement | null)?.blur()
206 }
207 }}>
208 {({hovered}) => (
209 <View style={{width: dimensions, height: dimensions}}>
210 <View
211 style={[
212 a.justify_center,
213 a.align_center,
214 a.transition_transform,
215 {
216 position: 'absolute',
217 top: 0,
218 left: 0,
219 right: 0,
220 bottom: 0,
221 transform: [{scale: hovered ? 1.1 : 1}],
222 },
223 ]}>
224 {icon}
225 </View>
226 </View>
227 )}
228 </Button>
229
230 <PdsDialog
231 control={dialogControl}
232 pdsUrl={pdsUrl}
233 faviconUrl={faviconUrl}
234 />
235 </>
236 )
237}