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