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