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