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