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