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 {logger} from '#/logger'
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 type * as bsky from '#/types/bsky'
25
26export {useDialogControl} from '#/components/Dialog'
27
28export function VerificationsDialog({
29 control,
30 profile,
31 verificationState,
32}: {
33 control: Dialog.DialogControlProps
34 profile: bsky.profile.AnyProfileView
35 verificationState: FullVerificationState
36}) {
37 return (
38 <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
39 <Dialog.Handle />
40 <Inner
41 control={control}
42 profile={profile}
43 verificationState={verificationState}
44 />
45 </Dialog.Outer>
46 )
47}
48
49function Inner({
50 profile,
51 control,
52 verificationState: state,
53}: {
54 control: Dialog.DialogControlProps
55 profile: bsky.profile.AnyProfileView
56 verificationState: FullVerificationState
57}) {
58 const t = useTheme()
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 logger.metric(
163 'verification:learn-more',
164 {
165 location: 'verificationsDialog',
166 },
167 {statsig: true},
168 )
169 }}>
170 <ButtonText>
171 <Trans context="english-only-resource">Learn more</Trans>
172 </ButtonText>
173 </Link>
174 </View>
175
176 <Dialog.Close />
177 </Dialog.ScrollableInner>
178 )
179}
180
181function VerifierCard({
182 verification,
183 subject,
184 outerDialogControl,
185}: {
186 verification: AppBskyActorDefs.VerificationView
187 subject: bsky.profile.AnyProfileView
188 outerDialogControl: Dialog.DialogControlProps
189}) {
190 const t = useTheme()
191 const {_, i18n} = useLingui()
192 const {currentAccount} = useSession()
193 const moderationOpts = useModerationOpts()
194 const {data: profile, error} = useProfileQuery({did: verification.issuer})
195 const verificationRemovePromptControl = useDialogControl()
196 const canAdminister = verification.issuer === currentAccount?.did
197
198 const enableSquareButtons = useEnableSquareButtons()
199
200 return (
201 <View
202 style={{
203 opacity: verification.isValid ? 1 : 0.5,
204 }}>
205 <ProfileCard.Outer>
206 <ProfileCard.Header>
207 {error ? (
208 <>
209 <ProfileCard.AvatarPlaceholder />
210 <View style={[a.flex_1]}>
211 <Text
212 style={[a.text_md, a.font_semi_bold, a.leading_snug]}
213 numberOfLines={1}>
214 <Trans>Unknown verifier</Trans>
215 </Text>
216 <Text
217 emoji
218 style={[a.leading_snug, t.atoms.text_contrast_medium]}
219 numberOfLines={1}>
220 {verification.issuer}
221 </Text>
222 </View>
223 </>
224 ) : profile && moderationOpts ? (
225 <>
226 <ProfileCard.Link
227 profile={profile}
228 style={[a.flex_row, a.align_center, a.gap_sm, a.flex_1]}
229 onPress={() => {
230 outerDialogControl.close()
231 }}>
232 <ProfileCard.Avatar
233 profile={profile}
234 moderationOpts={moderationOpts}
235 disabledPreview
236 />
237 <View style={[a.flex_1]}>
238 <ProfileCard.Name
239 profile={profile}
240 moderationOpts={moderationOpts}
241 />
242 <Text
243 emoji
244 style={[a.leading_snug, t.atoms.text_contrast_medium]}
245 numberOfLines={1}>
246 {i18n.date(new Date(verification.createdAt), {
247 dateStyle: 'long',
248 })}
249 </Text>
250 </View>
251 </ProfileCard.Link>
252 {canAdminister && (
253 <View>
254 <Button
255 label={_(msg`Remove verification`)}
256 size="small"
257 variant="outline"
258 color="negative"
259 shape={enableSquareButtons ? 'square' : 'round'}
260 onPress={() => {
261 verificationRemovePromptControl.open()
262 }}>
263 <ButtonIcon icon={TrashIcon} />
264 </Button>
265 </View>
266 )}
267 </>
268 ) : (
269 <>
270 <ProfileCard.AvatarPlaceholder />
271 <ProfileCard.NameAndHandlePlaceholder />
272 </>
273 )}
274 </ProfileCard.Header>
275 </ProfileCard.Outer>
276
277 <VerificationRemovePrompt
278 control={verificationRemovePromptControl}
279 profile={subject}
280 verifications={[verification]}
281 onConfirm={() => outerDialogControl.close()}
282 />
283 </View>
284 )
285}