mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {memo} from 'react'
2import {TouchableOpacity} from 'react-native'
3import {AppBskyActorDefs} from '@atproto/api'
4import {msg, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
7import {useQueryClient} from '@tanstack/react-query'
8import * as Toast from 'view/com/util/Toast'
9import {EventStopper} from 'view/com/util/EventStopper'
10import {useSession} from 'state/session'
11import * as Menu from '#/components/Menu'
12import {useTheme} from '#/alf'
13import {usePalette} from 'lib/hooks/usePalette'
14import {HITSLOP_10} from 'lib/constants'
15import {shareUrl} from 'lib/sharing'
16import {toShareUrl} from 'lib/strings/url-helpers'
17import {makeProfileLink} from 'lib/routes/links'
18import {useAnalytics} from 'lib/analytics/analytics'
19import {useModalControls} from 'state/modals'
20import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
21import {
22 RQKEY as profileQueryKey,
23 useProfileBlockMutationQueue,
24 useProfileFollowMutationQueue,
25 useProfileMuteMutationQueue,
26} from 'state/queries/profile'
27import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
28import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
29import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
30import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
31import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
32import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
33import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
34import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
35import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
36import {logger} from '#/logger'
37import {Shadow} from 'state/cache/types'
38import * as Prompt from '#/components/Prompt'
39
40let ProfileMenu = ({
41 profile,
42}: {
43 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
44}): React.ReactNode => {
45 const {_} = useLingui()
46 const {currentAccount, hasSession} = useSession()
47 const t = useTheme()
48 // TODO ALF this
49 const pal = usePalette('default')
50 const {track} = useAnalytics()
51 const {openModal} = useModalControls()
52 const reportDialogControl = useReportDialogControl()
53 const queryClient = useQueryClient()
54 const isSelf = currentAccount?.did === profile.did
55 const isFollowing = profile.viewer?.following
56 const isBlocked = profile.viewer?.blocking || profile.viewer?.blockedBy
57 const isFollowingBlockedAccount = isFollowing && isBlocked
58 const isLabelerAndNotBlocked = !!profile.associated?.labeler && !isBlocked
59
60 const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
61 const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
62 const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(
63 profile,
64 'ProfileMenu',
65 )
66
67 const blockPromptControl = Prompt.usePromptControl()
68 const loggedOutWarningPromptControl = Prompt.usePromptControl()
69
70 const showLoggedOutWarning = React.useMemo(() => {
71 return !!profile.labels?.find(label => label.val === '!no-unauthenticated')
72 }, [profile.labels])
73
74 const invalidateProfileQuery = React.useCallback(() => {
75 queryClient.invalidateQueries({
76 queryKey: profileQueryKey(profile.did),
77 })
78 }, [queryClient, profile.did])
79
80 const onPressShare = React.useCallback(() => {
81 track('ProfileHeader:ShareButtonClicked')
82 shareUrl(toShareUrl(makeProfileLink(profile)))
83 }, [track, profile])
84
85 const onPressAddRemoveLists = React.useCallback(() => {
86 track('ProfileHeader:AddToListsButtonClicked')
87 openModal({
88 name: 'user-add-remove-lists',
89 subject: profile.did,
90 handle: profile.handle,
91 displayName: profile.displayName || profile.handle,
92 onAdd: invalidateProfileQuery,
93 onRemove: invalidateProfileQuery,
94 })
95 }, [track, profile, openModal, invalidateProfileQuery])
96
97 const onPressMuteAccount = React.useCallback(async () => {
98 if (profile.viewer?.muted) {
99 track('ProfileHeader:UnmuteAccountButtonClicked')
100 try {
101 await queueUnmute()
102 Toast.show(_(msg`Account unmuted`))
103 } catch (e: any) {
104 if (e?.name !== 'AbortError') {
105 logger.error('Failed to unmute account', {message: e})
106 Toast.show(_(msg`There was an issue! ${e.toString()}`))
107 }
108 }
109 } else {
110 track('ProfileHeader:MuteAccountButtonClicked')
111 try {
112 await queueMute()
113 Toast.show(_(msg`Account muted`))
114 } catch (e: any) {
115 if (e?.name !== 'AbortError') {
116 logger.error('Failed to mute account', {message: e})
117 Toast.show(_(msg`There was an issue! ${e.toString()}`))
118 }
119 }
120 }
121 }, [profile.viewer?.muted, track, queueUnmute, _, queueMute])
122
123 const blockAccount = React.useCallback(async () => {
124 if (profile.viewer?.blocking) {
125 track('ProfileHeader:UnblockAccountButtonClicked')
126 try {
127 await queueUnblock()
128 Toast.show(_(msg`Account unblocked`))
129 } catch (e: any) {
130 if (e?.name !== 'AbortError') {
131 logger.error('Failed to unblock account', {message: e})
132 Toast.show(_(msg`There was an issue! ${e.toString()}`))
133 }
134 }
135 } else {
136 track('ProfileHeader:BlockAccountButtonClicked')
137 try {
138 await queueBlock()
139 Toast.show(_(msg`Account blocked`))
140 } catch (e: any) {
141 if (e?.name !== 'AbortError') {
142 logger.error('Failed to block account', {message: e})
143 Toast.show(_(msg`There was an issue! ${e.toString()}`))
144 }
145 }
146 }
147 }, [profile.viewer?.blocking, track, _, queueUnblock, queueBlock])
148
149 const onPressFollowAccount = React.useCallback(async () => {
150 track('ProfileHeader:FollowButtonClicked')
151 try {
152 await queueFollow()
153 Toast.show(_(msg`Account followed`))
154 } catch (e: any) {
155 if (e?.name !== 'AbortError') {
156 logger.error('Failed to follow account', {message: e})
157 Toast.show(_(msg`There was an issue! ${e.toString()}`))
158 }
159 }
160 }, [_, queueFollow, track])
161
162 const onPressUnfollowAccount = React.useCallback(async () => {
163 track('ProfileHeader:UnfollowButtonClicked')
164 try {
165 await queueUnfollow()
166 Toast.show(_(msg`Account unfollowed`))
167 } catch (e: any) {
168 if (e?.name !== 'AbortError') {
169 logger.error('Failed to unfollow account', {message: e})
170 Toast.show(_(msg`There was an issue! ${e.toString()}`))
171 }
172 }
173 }, [_, queueUnfollow, track])
174
175 const onPressReportAccount = React.useCallback(() => {
176 track('ProfileHeader:ReportAccountButtonClicked')
177 reportDialogControl.open()
178 }, [track, reportDialogControl])
179
180 return (
181 <EventStopper onKeyDown={false}>
182 <Menu.Root>
183 <Menu.Trigger label={_(`More options`)}>
184 {({props}) => {
185 return (
186 <TouchableOpacity
187 {...props}
188 hitSlop={HITSLOP_10}
189 testID="profileHeaderDropdownBtn"
190 style={[
191 {
192 flexDirection: 'row',
193 alignItems: 'center',
194 justifyContent: 'center',
195 paddingVertical: 10,
196 borderRadius: 50,
197 paddingHorizontal: 16,
198 },
199 pal.btn,
200 ]}>
201 <FontAwesomeIcon
202 icon="ellipsis"
203 size={20}
204 style={t.atoms.text}
205 />
206 </TouchableOpacity>
207 )
208 }}
209 </Menu.Trigger>
210
211 <Menu.Outer style={{minWidth: 170}}>
212 <Menu.Group>
213 <Menu.Item
214 testID="profileHeaderDropdownShareBtn"
215 label={_(msg`Share`)}
216 onPress={() => {
217 if (showLoggedOutWarning) {
218 loggedOutWarningPromptControl.open()
219 } else {
220 onPressShare()
221 }
222 }}>
223 <Menu.ItemText>
224 <Trans>Share</Trans>
225 </Menu.ItemText>
226 <Menu.ItemIcon icon={Share} />
227 </Menu.Item>
228 </Menu.Group>
229
230 {hasSession && (
231 <>
232 <Menu.Divider />
233 <Menu.Group>
234 {!isSelf && (
235 <>
236 {(isLabelerAndNotBlocked || isFollowingBlockedAccount) && (
237 <Menu.Item
238 testID="profileHeaderDropdownFollowBtn"
239 label={
240 isFollowing
241 ? _(msg`Unfollow Account`)
242 : _(msg`Follow Account`)
243 }
244 onPress={
245 isFollowing
246 ? onPressUnfollowAccount
247 : onPressFollowAccount
248 }>
249 <Menu.ItemText>
250 {isFollowing ? (
251 <Trans>Unfollow Account</Trans>
252 ) : (
253 <Trans>Follow Account</Trans>
254 )}
255 </Menu.ItemText>
256 <Menu.ItemIcon icon={isFollowing ? UserMinus : Plus} />
257 </Menu.Item>
258 )}
259 </>
260 )}
261 <Menu.Item
262 testID="profileHeaderDropdownListAddRemoveBtn"
263 label={_(msg`Add to Lists`)}
264 onPress={onPressAddRemoveLists}>
265 <Menu.ItemText>
266 <Trans>Add to Lists</Trans>
267 </Menu.ItemText>
268 <Menu.ItemIcon icon={List} />
269 </Menu.Item>
270 {!isSelf && (
271 <>
272 {!profile.viewer?.blocking &&
273 !profile.viewer?.mutedByList && (
274 <Menu.Item
275 testID="profileHeaderDropdownMuteBtn"
276 label={
277 profile.viewer?.muted
278 ? _(msg`Unmute Account`)
279 : _(msg`Mute Account`)
280 }
281 onPress={onPressMuteAccount}>
282 <Menu.ItemText>
283 {profile.viewer?.muted ? (
284 <Trans>Unmute Account</Trans>
285 ) : (
286 <Trans>Mute Account</Trans>
287 )}
288 </Menu.ItemText>
289 <Menu.ItemIcon
290 icon={profile.viewer?.muted ? Unmute : Mute}
291 />
292 </Menu.Item>
293 )}
294 {!profile.viewer?.blockingByList && (
295 <Menu.Item
296 testID="profileHeaderDropdownBlockBtn"
297 label={
298 profile.viewer
299 ? _(msg`Unblock Account`)
300 : _(msg`Block Account`)
301 }
302 onPress={() => blockPromptControl.open()}>
303 <Menu.ItemText>
304 {profile.viewer?.blocking ? (
305 <Trans>Unblock Account</Trans>
306 ) : (
307 <Trans>Block Account</Trans>
308 )}
309 </Menu.ItemText>
310 <Menu.ItemIcon
311 icon={
312 profile.viewer?.blocking ? PersonCheck : PersonX
313 }
314 />
315 </Menu.Item>
316 )}
317 <Menu.Item
318 testID="profileHeaderDropdownReportBtn"
319 label={_(msg`Report Account`)}
320 onPress={onPressReportAccount}>
321 <Menu.ItemText>
322 <Trans>Report Account</Trans>
323 </Menu.ItemText>
324 <Menu.ItemIcon icon={Flag} />
325 </Menu.Item>
326 </>
327 )}
328 </Menu.Group>
329 </>
330 )}
331 </Menu.Outer>
332 </Menu.Root>
333
334 <ReportDialog
335 control={reportDialogControl}
336 params={{type: 'account', did: profile.did}}
337 />
338
339 <Prompt.Basic
340 control={blockPromptControl}
341 title={
342 profile.viewer?.blocking
343 ? _(msg`Unblock Account?`)
344 : _(msg`Block Account?`)
345 }
346 description={
347 profile.viewer?.blocking
348 ? _(
349 msg`The account will be able to interact with you after unblocking.`,
350 )
351 : profile.associated?.labeler
352 ? _(
353 msg`Blocking will not prevent labels from being applied on your account, but it will stop this account from replying in your threads or interacting with you.`,
354 )
355 : _(
356 msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
357 )
358 }
359 onConfirm={blockAccount}
360 confirmButtonCta={
361 profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`)
362 }
363 confirmButtonColor={profile.viewer?.blocking ? undefined : 'negative'}
364 />
365
366 <Prompt.Basic
367 control={loggedOutWarningPromptControl}
368 title={_(msg`Note about sharing`)}
369 description={_(
370 msg`This profile is only visible to logged-in users. It won't be visible to people who aren't logged in.`,
371 )}
372 onConfirm={onPressShare}
373 confirmButtonCta={_(msg`Share anyway`)}
374 />
375 </EventStopper>
376 )
377}
378
379ProfileMenu = memo(ProfileMenu)
380export {ProfileMenu}