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 moderateProfile,
6 ModerationOpts,
7 RichText as RichTextAPI,
8} from '@atproto/api'
9import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
10import {msg, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12
13import {useGate} from '#/lib/statsig/statsig'
14import {logger} from '#/logger'
15import {isIOS} from '#/platform/detection'
16import {Shadow} from '#/state/cache/types'
17import {useModalControls} from '#/state/modals'
18import {
19 useProfileBlockMutationQueue,
20 useProfileFollowMutationQueue,
21} from '#/state/queries/profile'
22import {useRequireAuth, useSession} from '#/state/session'
23import {useAnalytics} from 'lib/analytics/analytics'
24import {sanitizeDisplayName} from 'lib/strings/display-names'
25import {useProfileShadow} from 'state/cache/profile-shadow'
26import {ProfileHeaderSuggestedFollows} from '#/view/com/profile/ProfileHeaderSuggestedFollows'
27import {ProfileMenu} from '#/view/com/profile/ProfileMenu'
28import * as Toast from '#/view/com/util/Toast'
29import {atoms as a, useTheme} from '#/alf'
30import {Button, ButtonIcon, ButtonText} from '#/components/Button'
31import {MessageProfileButton} from '#/components/dms/MessageProfileButton'
32import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
33import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
34import {
35 KnownFollowers,
36 shouldShowKnownFollowers,
37} from '#/components/KnownFollowers'
38import * as Prompt from '#/components/Prompt'
39import {RichText} from '#/components/RichText'
40import {ProfileHeaderDisplayName} from './DisplayName'
41import {ProfileHeaderHandle} from './Handle'
42import {ProfileHeaderMetrics} from './Metrics'
43import {ProfileHeaderShell} from './Shell'
44
45interface Props {
46 profile: AppBskyActorDefs.ProfileViewDetailed
47 descriptionRT: RichTextAPI | null
48 moderationOpts: ModerationOpts
49 hideBackButton?: boolean
50 isPlaceholderProfile?: boolean
51}
52
53let ProfileHeaderStandard = ({
54 profile: profileUnshadowed,
55 descriptionRT,
56 moderationOpts,
57 hideBackButton = false,
58 isPlaceholderProfile,
59}: Props): React.ReactNode => {
60 const profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> =
61 useProfileShadow(profileUnshadowed)
62 const t = useTheme()
63 const gate = useGate()
64 const {currentAccount, hasSession} = useSession()
65 const {_} = useLingui()
66 const {openModal} = useModalControls()
67 const {track} = useAnalytics()
68 const moderation = useMemo(
69 () => moderateProfile(profile, moderationOpts),
70 [profile, moderationOpts],
71 )
72 const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
73 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
74 profile,
75 'ProfileHeader',
76 )
77 const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
78 const unblockPromptControl = Prompt.usePromptControl()
79 const requireAuth = useRequireAuth()
80 const isBlockedUser =
81 profile.viewer?.blocking ||
82 profile.viewer?.blockedBy ||
83 profile.viewer?.blockingByList
84
85 const onPressEditProfile = React.useCallback(() => {
86 track('ProfileHeader:EditProfileButtonClicked')
87 openModal({
88 name: 'edit-profile',
89 profile,
90 })
91 }, [track, openModal, profile])
92
93 const onPressFollow = () => {
94 requireAuth(async () => {
95 try {
96 track('ProfileHeader:FollowButtonClicked')
97 await queueFollow()
98 Toast.show(
99 _(
100 msg`Following ${sanitizeDisplayName(
101 profile.displayName || profile.handle,
102 moderation.ui('displayName'),
103 )}`,
104 ),
105 )
106 } catch (e: any) {
107 if (e?.name !== 'AbortError') {
108 logger.error('Failed to follow', {message: String(e)})
109 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
110 }
111 }
112 })
113 }
114
115 const onPressUnfollow = () => {
116 requireAuth(async () => {
117 try {
118 track('ProfileHeader:UnfollowButtonClicked')
119 await queueUnfollow()
120 Toast.show(
121 _(
122 msg`No longer following ${sanitizeDisplayName(
123 profile.displayName || profile.handle,
124 moderation.ui('displayName'),
125 )}`,
126 ),
127 )
128 } catch (e: any) {
129 if (e?.name !== 'AbortError') {
130 logger.error('Failed to unfollow', {message: String(e)})
131 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
132 }
133 }
134 })
135 }
136
137 const unblockAccount = React.useCallback(async () => {
138 track('ProfileHeader:UnblockAccountButtonClicked')
139 try {
140 await queueUnblock()
141 Toast.show(_(msg`Account unblocked`))
142 } catch (e: any) {
143 if (e?.name !== 'AbortError') {
144 logger.error('Failed to unblock account', {message: e})
145 Toast.show(_(msg`There was an issue! ${e.toString()}`), 'xmark')
146 }
147 }
148 }, [_, queueUnblock, track])
149
150 const isMe = React.useMemo(
151 () => currentAccount?.did === profile.did,
152 [currentAccount, profile],
153 )
154
155 return (
156 <ProfileHeaderShell
157 profile={profile}
158 moderation={moderation}
159 hideBackButton={hideBackButton}
160 isPlaceholderProfile={isPlaceholderProfile}>
161 <View
162 style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]}
163 pointerEvents={isIOS ? 'auto' : 'box-none'}>
164 <View
165 style={[
166 {paddingLeft: 90},
167 a.flex_row,
168 a.justify_end,
169 a.gap_sm,
170 a.pb_sm,
171 a.flex_wrap,
172 ]}
173 pointerEvents={isIOS ? 'auto' : 'box-none'}>
174 {isMe ? (
175 <Button
176 testID="profileHeaderEditProfileButton"
177 size="small"
178 color="secondary"
179 variant="solid"
180 onPress={onPressEditProfile}
181 label={_(msg`Edit profile`)}
182 style={[a.rounded_full, a.py_sm]}>
183 <ButtonText>
184 <Trans>Edit Profile</Trans>
185 </ButtonText>
186 </Button>
187 ) : profile.viewer?.blocking ? (
188 profile.viewer?.blockingByList ? null : (
189 <Button
190 testID="unblockBtn"
191 size="small"
192 color="secondary"
193 variant="solid"
194 label={_(msg`Unblock`)}
195 disabled={!hasSession}
196 onPress={() => unblockPromptControl.open()}
197 style={[a.rounded_full, a.py_sm]}>
198 <ButtonText>
199 <Trans context="action">Unblock</Trans>
200 </ButtonText>
201 </Button>
202 )
203 ) : !profile.viewer?.blockedBy ? (
204 <>
205 {hasSession && (
206 <>
207 <MessageProfileButton profile={profile} />
208 {!gate('show_follow_suggestions_in_profile') && (
209 <Button
210 testID="suggestedFollowsBtn"
211 size="small"
212 color={showSuggestedFollows ? 'primary' : 'secondary'}
213 variant="solid"
214 shape="round"
215 onPress={() =>
216 setShowSuggestedFollows(!showSuggestedFollows)
217 }
218 label={_(msg`Show follows similar to ${profile.handle}`)}
219 style={{width: 36, height: 36}}>
220 <FontAwesomeIcon
221 icon="user-plus"
222 style={
223 showSuggestedFollows
224 ? {color: t.palette.white}
225 : t.atoms.text
226 }
227 size={14}
228 />
229 </Button>
230 )}
231 </>
232 )}
233
234 <Button
235 testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'}
236 size="small"
237 color={profile.viewer?.following ? 'secondary' : 'primary'}
238 variant="solid"
239 label={
240 profile.viewer?.following
241 ? _(msg`Unfollow ${profile.handle}`)
242 : _(msg`Follow ${profile.handle}`)
243 }
244 onPress={
245 profile.viewer?.following ? onPressUnfollow : onPressFollow
246 }
247 style={[a.rounded_full, a.gap_xs, a.py_sm]}>
248 <ButtonIcon
249 position="left"
250 icon={profile.viewer?.following ? Check : Plus}
251 />
252 <ButtonText>
253 {profile.viewer?.following ? (
254 <Trans>Following</Trans>
255 ) : (
256 <Trans>Follow</Trans>
257 )}
258 </ButtonText>
259 </Button>
260 </>
261 ) : null}
262 <ProfileMenu profile={profile} />
263 </View>
264 <View style={[a.flex_col, a.gap_xs, a.pb_sm]}>
265 <ProfileHeaderDisplayName profile={profile} moderation={moderation} />
266 <ProfileHeaderHandle profile={profile} />
267 </View>
268 {!isPlaceholderProfile && !isBlockedUser && (
269 <>
270 <ProfileHeaderMetrics profile={profile} />
271 {descriptionRT && !moderation.ui('profileView').blur ? (
272 <View pointerEvents="auto">
273 <RichText
274 testID="profileHeaderDescription"
275 style={[a.text_md]}
276 numberOfLines={15}
277 value={descriptionRT}
278 enableTags
279 authorHandle={profile.handle}
280 />
281 </View>
282 ) : undefined}
283
284 {!isMe &&
285 !isBlockedUser &&
286 shouldShowKnownFollowers(profile.viewer?.knownFollowers) && (
287 <View style={[a.flex_row, a.align_center, a.gap_sm, a.pt_md]}>
288 <KnownFollowers
289 profile={profile}
290 moderationOpts={moderationOpts}
291 />
292 </View>
293 )}
294 </>
295 )}
296 </View>
297 {showSuggestedFollows && (
298 <ProfileHeaderSuggestedFollows
299 actorDid={profile.did}
300 requestDismiss={() => {
301 if (showSuggestedFollows) {
302 setShowSuggestedFollows(false)
303 } else {
304 track('ProfileHeader:SuggestedFollowsOpened')
305 setShowSuggestedFollows(true)
306 }
307 }}
308 />
309 )}
310 <Prompt.Basic
311 control={unblockPromptControl}
312 title={_(msg`Unblock Account?`)}
313 description={_(
314 msg`The account will be able to interact with you after unblocking.`,
315 )}
316 onConfirm={unblockAccount}
317 confirmButtonCta={
318 profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
319 }
320 confirmButtonColor="negative"
321 />
322 </ProfileHeaderShell>
323 )
324}
325ProfileHeaderStandard = memo(ProfileHeaderStandard)
326export {ProfileHeaderStandard}