mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at quote-hover 380 lines 14 kB view raw
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}