mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at verify-code 384 lines 14 kB view raw
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}