Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1import {View} from 'react-native'
2import {type AppBskyActorDefs} from '@atproto/api'
3import {msg} from '@lingui/core/macro'
4import {useLingui} from '@lingui/react'
5import {Trans} from '@lingui/react/macro'
6
7import {urls} from '#/lib/constants'
8import {getUserDisplayName} from '#/lib/getUserDisplayName'
9import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'
10import {useModerationOpts} from '#/state/preferences/moderation-opts'
11import {useProfileQuery} from '#/state/queries/profile'
12import {useSession} from '#/state/session'
13import {atoms as a, useBreakpoints, useTheme} from '#/alf'
14import {Admonition} from '#/components/Admonition'
15import {Button, ButtonIcon, ButtonText} from '#/components/Button'
16import * as Dialog from '#/components/Dialog'
17import {useDialogControl} from '#/components/Dialog'
18import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
19import {Link} from '#/components/Link'
20import * as ProfileCard from '#/components/ProfileCard'
21import {Text} from '#/components/Typography'
22import {type FullVerificationState} from '#/components/verification'
23import {VerificationRemovePrompt} from '#/components/verification/VerificationRemovePrompt'
24import {useAnalytics} from '#/analytics'
25import type * as bsky from '#/types/bsky'
26
27export {useDialogControl} from '#/components/Dialog'
28
29export function VerificationsDialog({
30 control,
31 profile,
32 verificationState,
33}: {
34 control: Dialog.DialogControlProps
35 profile: bsky.profile.AnyProfileView
36 verificationState: FullVerificationState
37}) {
38 return (
39 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
40 <Dialog.Handle />
41 <Inner
42 control={control}
43 profile={profile}
44 verificationState={verificationState}
45 />
46 </Dialog.Outer>
47 )
48}
49
50function Inner({
51 profile,
52 control,
53 verificationState: state,
54}: {
55 control: Dialog.DialogControlProps
56 profile: bsky.profile.AnyProfileView
57 verificationState: FullVerificationState
58}) {
59 const t = useTheme()
60 const ax = useAnalytics()
61 const {_} = useLingui()
62 const {gtMobile} = useBreakpoints()
63
64 const userName = getUserDisplayName(profile)
65 const label = state.profile.isViewer
66 ? state.profile.isVerified
67 ? _(msg`You are verified`)
68 : _(msg`Your verifications`)
69 : state.profile.isVerified
70 ? _(msg`${userName} is verified`)
71 : _(
72 msg({
73 message: `${userName}'s verifications`,
74 comment: `Possessive, meaning "the verifications of {userName}"`,
75 }),
76 )
77
78 return (
79 <Dialog.ScrollableInner
80 label={label}
81 style={[
82 gtMobile ? {width: 'auto', maxWidth: 400, minWidth: 200} : a.w_full,
83 ]}>
84 <View style={[a.gap_sm, a.pb_lg]}>
85 <Text style={[a.text_2xl, a.font_semi_bold, a.pr_4xl, a.leading_tight]}>
86 {label}
87 </Text>
88 <Text style={[a.text_md, a.leading_snug]}>
89 {state.profile.isVerified ? (
90 <Trans>
91 This account has a checkmark because it's been verified by trusted
92 sources.
93 </Trans>
94 ) : (
95 <Trans>
96 This account has one or more attempted verifications, but it is
97 not currently verified.
98 </Trans>
99 )}
100 </Text>
101 </View>
102
103 {profile.verification ? (
104 <View style={[a.pb_xl, a.gap_md]}>
105 <Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
106 <Trans>Verified by:</Trans>
107 </Text>
108
109 <View style={[a.gap_lg]}>
110 {profile.verification.verifications.map(v => (
111 <VerifierCard
112 key={v.uri}
113 verification={v}
114 subject={profile}
115 outerDialogControl={control}
116 />
117 ))}
118 </View>
119
120 {profile.verification.verifications.some(v => !v.isValid) &&
121 state.profile.isViewer && (
122 <Admonition type="warning" style={[a.mt_xs]}>
123 <Trans>Some of your verifications are invalid.</Trans>
124 </Admonition>
125 )}
126 </View>
127 ) : null}
128
129 <View
130 style={[
131 a.w_full,
132 a.gap_sm,
133 a.justify_end,
134 gtMobile
135 ? [a.flex_row, a.flex_row_reverse, a.justify_start]
136 : [a.flex_col],
137 ]}>
138 <Button
139 label={_(msg`Close dialog`)}
140 size="small"
141 variant="solid"
142 color="primary"
143 onPress={() => {
144 control.close()
145 }}>
146 <ButtonText>
147 <Trans>Close</Trans>
148 </ButtonText>
149 </Button>
150 <Link
151 overridePresentation
152 to={urls.website.blog.initialVerificationAnnouncement}
153 label={_(
154 msg({
155 message: `Learn more about verification on Bluesky`,
156 context: `english-only-resource`,
157 }),
158 )}
159 size="small"
160 variant="solid"
161 color="secondary"
162 style={[a.justify_center]}
163 onPress={() => {
164 ax.metric('verification:learn-more', {
165 location: 'verificationsDialog',
166 })
167 }}>
168 <ButtonText>
169 <Trans context="english-only-resource">Learn more</Trans>
170 </ButtonText>
171 </Link>
172 </View>
173
174 <Dialog.Close />
175 </Dialog.ScrollableInner>
176 )
177}
178
179function VerifierCard({
180 verification,
181 subject,
182 outerDialogControl,
183}: {
184 verification: AppBskyActorDefs.VerificationView
185 subject: bsky.profile.AnyProfileView
186 outerDialogControl: Dialog.DialogControlProps
187}) {
188 const t = useTheme()
189 const {_, i18n} = useLingui()
190 const {currentAccount} = useSession()
191 const moderationOpts = useModerationOpts()
192 const {data: profile, error} = useProfileQuery({did: verification.issuer})
193 const verificationRemovePromptControl = useDialogControl()
194 const canAdminister = verification.issuer === currentAccount?.did
195
196 const enableSquareButtons = useEnableSquareButtons()
197
198 return (
199 <View
200 style={{
201 opacity: verification.isValid ? 1 : 0.5,
202 }}>
203 <ProfileCard.Outer>
204 <ProfileCard.Header>
205 {error ? (
206 <>
207 <ProfileCard.AvatarPlaceholder />
208 <View style={[a.flex_1]}>
209 <Text
210 style={[a.text_md, a.font_semi_bold, a.leading_snug]}
211 numberOfLines={1}>
212 <Trans>Unknown verifier</Trans>
213 </Text>
214 <Text
215 emoji
216 style={[a.leading_snug, t.atoms.text_contrast_medium]}
217 numberOfLines={1}>
218 {verification.issuer}
219 </Text>
220 </View>
221 </>
222 ) : profile && moderationOpts ? (
223 <>
224 <ProfileCard.Link
225 profile={profile}
226 style={[a.flex_row, a.align_center, a.gap_sm, a.flex_1]}
227 onPress={() => {
228 outerDialogControl.close()
229 }}>
230 <ProfileCard.Avatar
231 profile={profile}
232 moderationOpts={moderationOpts}
233 disabledPreview
234 />
235 <View style={[a.flex_1]}>
236 <ProfileCard.Name
237 profile={profile}
238 moderationOpts={moderationOpts}
239 />
240 <Text
241 emoji
242 style={[a.leading_snug, t.atoms.text_contrast_medium]}
243 numberOfLines={1}>
244 {i18n.date(new Date(verification.createdAt), {
245 dateStyle: 'long',
246 })}
247 </Text>
248 </View>
249 </ProfileCard.Link>
250 {canAdminister && (
251 <View>
252 <Button
253 label={_(msg`Remove verification`)}
254 size="small"
255 variant="outline"
256 color="negative"
257 shape={enableSquareButtons ? 'square' : 'round'}
258 onPress={() => {
259 verificationRemovePromptControl.open()
260 }}>
261 <ButtonIcon icon={TrashIcon} />
262 </Button>
263 </View>
264 )}
265 </>
266 ) : (
267 <>
268 <ProfileCard.AvatarPlaceholder />
269 <ProfileCard.NameAndHandlePlaceholder />
270 </>
271 )}
272 </ProfileCard.Header>
273 </ProfileCard.Outer>
274
275 <VerificationRemovePrompt
276 control={verificationRemovePromptControl}
277 profile={subject}
278 verifications={[verification]}
279 onConfirm={() => outerDialogControl.close()}
280 />
281 </View>
282 )
283}