mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {memo, useMemo} from 'react'
2import {View} from 'react-native'
3import {
4 type AppBskyActorDefs,
5 type AppBskyLabelerDefs,
6 moderateProfile,
7 type ModerationOpts,
8 type RichText as RichTextAPI,
9} from '@atproto/api'
10import {msg, Plural, plural, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12
13// eslint-disable-next-line @typescript-eslint/no-unused-vars
14import {MAX_LABELERS} from '#/lib/constants'
15import {useHaptics} from '#/lib/haptics'
16import {isAppLabeler} from '#/lib/moderation'
17import {logger} from '#/logger'
18import {isIOS} from '#/platform/detection'
19import {useProfileShadow} from '#/state/cache/profile-shadow'
20import {type Shadow} from '#/state/cache/types'
21import {useLabelerSubscriptionMutation} from '#/state/queries/labeler'
22import {useLikeMutation, useUnlikeMutation} from '#/state/queries/like'
23import {usePreferencesQuery} from '#/state/queries/preferences'
24import {useRequireAuth, useSession} from '#/state/session'
25import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
26import * as Toast from '#/view/com/util/Toast'
27import {atoms as a, tokens, useTheme} from '#/alf'
28import {Button, ButtonText} from '#/components/Button'
29import {type DialogOuterProps, useDialogControl} from '#/components/Dialog'
30import {
31 Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
32 Heart2_Stroke2_Corner0_Rounded as Heart,
33} from '#/components/icons/Heart2'
34import {Link} from '#/components/Link'
35import * as Prompt from '#/components/Prompt'
36import {RichText} from '#/components/RichText'
37import {Text} from '#/components/Typography'
38import {ProfileHeaderDisplayName} from './DisplayName'
39import {EditProfileDialog} from './EditProfileDialog'
40import {ProfileHeaderHandle} from './Handle'
41import {ProfileHeaderMetrics} from './Metrics'
42import {ProfileHeaderShell} from './Shell'
43
44interface Props {
45 profile: AppBskyActorDefs.ProfileViewDetailed
46 labeler: AppBskyLabelerDefs.LabelerViewDetailed
47 descriptionRT: RichTextAPI | null
48 moderationOpts: ModerationOpts
49 hideBackButton?: boolean
50 isPlaceholderProfile?: boolean
51}
52
53let ProfileHeaderLabeler = ({
54 profile: profileUnshadowed,
55 labeler,
56 descriptionRT,
57 moderationOpts,
58 hideBackButton = false,
59 isPlaceholderProfile,
60}: Props): React.ReactNode => {
61 const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
62 useProfileShadow(profileUnshadowed)
63 const t = useTheme()
64 const {_} = useLingui()
65 const {currentAccount, hasSession} = useSession()
66 const requireAuth = useRequireAuth()
67 const playHaptic = useHaptics()
68 const cantSubscribePrompt = Prompt.usePromptControl()
69 const isSelf = currentAccount?.did === profile.did
70
71 const moderation = useMemo(
72 () => moderateProfile(profile, moderationOpts),
73 [profile, moderationOpts],
74 )
75 const {data: preferences} = usePreferencesQuery()
76 const {
77 mutateAsync: toggleSubscription,
78 variables,
79 reset,
80 } = useLabelerSubscriptionMutation()
81 const isSubscribed =
82 variables?.subscribe ??
83 preferences?.moderationPrefs.labelers.find(l => l.did === profile.did)
84 const {mutateAsync: likeMod, isPending: isLikePending} = useLikeMutation()
85 const {mutateAsync: unlikeMod, isPending: isUnlikePending} =
86 useUnlikeMutation()
87 const [likeUri, setLikeUri] = React.useState<string>(
88 labeler.viewer?.like || '',
89 )
90 const [likeCount, setLikeCount] = React.useState(labeler.likeCount || 0)
91
92 const onToggleLiked = React.useCallback(async () => {
93 if (!labeler) {
94 return
95 }
96 try {
97 playHaptic()
98
99 if (likeUri) {
100 await unlikeMod({uri: likeUri})
101 setLikeCount(c => c - 1)
102 setLikeUri('')
103 } else {
104 const res = await likeMod({uri: labeler.uri, cid: labeler.cid})
105 setLikeCount(c => c + 1)
106 setLikeUri(res.uri)
107 }
108 } catch (e: any) {
109 Toast.show(
110 _(
111 msg`There was an issue contacting the server, please check your internet connection and try again.`,
112 ),
113 'xmark',
114 )
115 logger.error(`Failed to toggle labeler like`, {message: e.message})
116 }
117 }, [labeler, playHaptic, likeUri, unlikeMod, likeMod, _])
118
119 const editProfileControl = useDialogControl()
120
121 const onPressSubscribe = React.useCallback(
122 () =>
123 requireAuth(async (): Promise<void> => {
124 const subscribe = !isSubscribed
125
126 try {
127 await toggleSubscription({
128 did: profile.did,
129 subscribe,
130 })
131
132 logger.metric(
133 subscribe
134 ? 'moderation:subscribedToLabeler'
135 : 'moderation:unsubscribedFromLabeler',
136 {},
137 {statsig: true},
138 )
139 } catch (e: any) {
140 reset()
141 if (e.message === 'MAX_LABELERS') {
142 cantSubscribePrompt.open()
143 return
144 }
145 logger.error(`Failed to subscribe to labeler`, {message: e.message})
146 }
147 }),
148 [
149 requireAuth,
150 toggleSubscription,
151 isSubscribed,
152 profile,
153 cantSubscribePrompt,
154 reset,
155 ],
156 )
157
158 const isMe = React.useMemo(
159 () => currentAccount?.did === profile.did,
160 [currentAccount, profile],
161 )
162
163 return (
164 <ProfileHeaderShell
165 profile={profile}
166 moderation={moderation}
167 hideBackButton={hideBackButton}
168 isPlaceholderProfile={isPlaceholderProfile}>
169 <View
170 style={[a.px_lg, a.pt_md, a.pb_sm]}
171 pointerEvents={isIOS ? 'auto' : 'box-none'}>
172 <View
173 style={[a.flex_row, a.justify_end, a.align_center, a.gap_xs, a.pb_lg]}
174 pointerEvents={isIOS ? 'auto' : 'box-none'}>
175 {isMe ? (
176 <>
177 <Button
178 testID="profileHeaderEditProfileButton"
179 size="small"
180 color="secondary"
181 variant="solid"
182 onPress={editProfileControl.open}
183 label={_(msg`Edit profile`)}
184 style={a.rounded_full}>
185 <ButtonText>
186 <Trans>Edit Profile</Trans>
187 </ButtonText>
188 </Button>
189 <EditProfileDialog
190 profile={profile}
191 control={editProfileControl}
192 />
193 </>
194 ) : !isAppLabeler(profile.did) ? (
195 <>
196 <Button
197 testID="toggleSubscribeBtn"
198 label={
199 isSubscribed
200 ? _(msg`Unsubscribe from this labeler`)
201 : _(msg`Subscribe to this labeler`)
202 }
203 onPress={onPressSubscribe}>
204 {state => (
205 <View
206 style={[
207 {
208 paddingVertical: 9,
209 paddingHorizontal: 12,
210 borderRadius: 6,
211 gap: 6,
212 backgroundColor: isSubscribed
213 ? state.hovered || state.pressed
214 ? t.palette.contrast_50
215 : t.palette.contrast_25
216 : state.hovered || state.pressed
217 ? tokens.color.temp_purple_dark
218 : tokens.color.temp_purple,
219 },
220 ]}>
221 <Text
222 style={[
223 {
224 color: isSubscribed
225 ? t.palette.contrast_700
226 : t.palette.white,
227 },
228 a.font_bold,
229 a.text_center,
230 a.leading_tight,
231 ]}>
232 {isSubscribed ? (
233 <Trans>Unsubscribe</Trans>
234 ) : (
235 <Trans>Subscribe to Labeler</Trans>
236 )}
237 </Text>
238 </View>
239 )}
240 </Button>
241 </>
242 ) : null}
243 <ProfileMenu profile={profile} />
244 </View>
245 <View style={[a.flex_col, a.gap_2xs, a.pt_2xs, a.pb_md]}>
246 <ProfileHeaderDisplayName profile={profile} moderation={moderation} />
247 <ProfileHeaderHandle profile={profile} />
248 </View>
249 {!isPlaceholderProfile && (
250 <>
251 {isSelf && <ProfileHeaderMetrics profile={profile} />}
252 {descriptionRT && !moderation.ui('profileView').blur ? (
253 <View pointerEvents="auto">
254 <RichText
255 testID="profileHeaderDescription"
256 style={[a.text_md]}
257 numberOfLines={15}
258 value={descriptionRT}
259 enableTags
260 authorHandle={profile.handle}
261 />
262 </View>
263 ) : undefined}
264 {!isAppLabeler(profile.did) && (
265 <View style={[a.flex_row, a.gap_xs, a.align_center, a.pt_lg]}>
266 <Button
267 testID="toggleLikeBtn"
268 size="small"
269 color="secondary"
270 variant="solid"
271 shape="round"
272 label={_(msg`Like this labeler`)}
273 disabled={!hasSession || isLikePending || isUnlikePending}
274 onPress={onToggleLiked}>
275 {likeUri ? (
276 <HeartFilled fill={t.palette.negative_400} />
277 ) : (
278 <Heart fill={t.atoms.text_contrast_medium.color} />
279 )}
280 </Button>
281
282 {typeof likeCount === 'number' && (
283 <Link
284 to={{
285 screen: 'ProfileLabelerLikedBy',
286 params: {
287 name: labeler.creator.handle || labeler.creator.did,
288 },
289 }}
290 size="tiny"
291 label={_(
292 msg`Liked by ${plural(likeCount, {
293 one: '# user',
294 other: '# users',
295 })}`,
296 )}>
297 {({hovered, focused, pressed}) => (
298 <Text
299 style={[
300 a.font_bold,
301 a.text_sm,
302 t.atoms.text_contrast_medium,
303 (hovered || focused || pressed) &&
304 t.atoms.text_contrast_high,
305 ]}>
306 <Trans>
307 Liked by{' '}
308 <Plural
309 value={likeCount}
310 one="# user"
311 other="# users"
312 />
313 </Trans>
314 </Text>
315 )}
316 </Link>
317 )}
318 </View>
319 )}
320 </>
321 )}
322 </View>
323 <CantSubscribePrompt control={cantSubscribePrompt} />
324 </ProfileHeaderShell>
325 )
326}
327ProfileHeaderLabeler = memo(ProfileHeaderLabeler)
328export {ProfileHeaderLabeler}
329
330/**
331 * Keep this in sync with the value of {@link MAX_LABELERS}
332 */
333function CantSubscribePrompt({
334 control,
335}: {
336 control: DialogOuterProps['control']
337}) {
338 const {_} = useLingui()
339 return (
340 <Prompt.Outer control={control}>
341 <Prompt.TitleText>Unable to subscribe</Prompt.TitleText>
342 <Prompt.DescriptionText>
343 <Trans>
344 We're sorry! You can only subscribe to twenty labelers, and you've
345 reached your limit of twenty.
346 </Trans>
347 </Prompt.DescriptionText>
348 <Prompt.Actions>
349 <Prompt.Action onPress={() => control.close()} cta={_(msg`OK`)} />
350 </Prompt.Actions>
351 </Prompt.Outer>
352 )
353}