Use new menu for Profile (#3168)
* use new menu on profile
* organize imports
* fix testID
* add person icons
* use `style` prop for minWidth
* use new icons
* rm circleban
* Add unfollow option if account is blocked/blocking
* use `StyleProp` 🤯
* ts after merge
---------
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
zio/stable
parent
70ad820d64
commit
090b35e52e
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M4 4a2 2 0 0 1 2-2h13.131c1.598 0 2.55 1.78 1.665 3.11L18.202 9l2.594 3.89c.886 1.33-.067 3.11-1.665 3.11H6v5a1 1 0 1 1-2 0V4Zm2 10h13.131l-2.593-3.89a2 2 0 0 1 0-2.22L19.13 4H6v10Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 323 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#000" fill-rule="evenodd" d="M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 466 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 547 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 625 B |
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View, Pressable} from 'react-native'
|
import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
|
||||||
import flattenReactChildren from 'react-keyed-flatten-children'
|
import flattenReactChildren from 'react-keyed-flatten-children'
|
||||||
|
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
@ -75,7 +75,10 @@ export function Trigger({children, label}: TriggerProps) {
|
||||||
export function Outer({
|
export function Outer({
|
||||||
children,
|
children,
|
||||||
showCancel,
|
showCancel,
|
||||||
}: React.PropsWithChildren<{showCancel?: boolean}>) {
|
}: React.PropsWithChildren<{
|
||||||
|
showCancel?: boolean
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}>) {
|
||||||
const context = React.useContext(Context)
|
const context = React.useContext(Context)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable react/prop-types */
|
/* eslint-disable react/prop-types */
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View, Pressable} from 'react-native'
|
import {View, Pressable, ViewStyle, StyleProp} from 'react-native'
|
||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||||
|
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
@ -132,7 +132,13 @@ export function Trigger({children, label}: TriggerProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outer({children}: React.PropsWithChildren<{}>) {
|
export function Outer({
|
||||||
|
children,
|
||||||
|
style,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
showCancel?: boolean
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
}>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -144,6 +150,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
|
||||||
a.p_xs,
|
a.p_xs,
|
||||||
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
|
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
|
||||||
t.atoms.shadow_md,
|
t.atoms.shadow_md,
|
||||||
|
style,
|
||||||
]}>
|
]}>
|
||||||
{children}
|
{children}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Flag_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4 4a2 2 0 0 1 2-2h13.131c1.598 0 2.55 1.78 1.665 3.11L18.202 9l2.594 3.89c.886 1.33-.067 3.11-1.665 3.11H6v5a1 1 0 1 1-2 0V4Zm2 10h13.131l-2.593-3.89a2 2 0 0 1 0-2.22L19.13 4H6v10Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const PeopleRemove2_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M10 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM5.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM16 11a1 1 0 0 1 1-1h5a1 1 0 1 1 0 2h-5a1 1 0 0 1-1-1ZM3.678 19h12.644c-.71-2.909-3.092-5-6.322-5s-5.613 2.091-6.322 5Zm-2.174.906C1.917 15.521 5.242 12 10 12c4.758 0 8.083 3.521 8.496 7.906A1 1 0 0 1 17.5 21h-15a1 1 0 0 1-.996-1.094Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const PersonCheck_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5a6.69 6.69 0 0 1 2.612.51 1 1 0 0 0 .776-1.844A8.687 8.687 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H11a1 1 0 1 0 0-2H5.679Zm14.835-4.857a1 1 0 0 1 .344 1.371l-3 5a1 1 0 0 1-1.458.286l-2-1.5a1 1 0 0 1 1.2-1.6l1.113.835 2.43-4.05a1 1 0 0 1 1.372-.342Z',
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const PersonX_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M12 4a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM7.5 6.5a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM5.679 19c.709-2.902 3.079-5 6.321-5 .302 0 .595.018.878.053a1 1 0 0 0 .243-1.985A9.235 9.235 0 0 0 12 12c-4.3 0-7.447 2.884-8.304 6.696-.29 1.29.767 2.304 1.902 2.304H12a1 1 0 1 0 0-2H5.679Zm9.614-3.707a1 1 0 0 1 1.414 0L18 16.586l1.293-1.293a1 1 0 0 1 1.414 1.414L19.414 18l1.293 1.293a1 1 0 0 1-1.414 1.414L18 19.414l-1.293 1.293a1 1 0 0 1-1.414-1.414L16.586 18l-1.293-1.293a1 1 0 0 1 0-1.414Z',
|
||||||
|
})
|
|
@ -7,7 +7,6 @@ import {
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
ModerationOpts,
|
ModerationOpts,
|
||||||
|
@ -17,7 +16,7 @@ import {
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {NavigationProp} from 'lib/routes/types'
|
import {NavigationProp} from 'lib/routes/types'
|
||||||
import {isNative, isWeb} from 'platform/detection'
|
import {isNative} from 'platform/detection'
|
||||||
import {BlurView} from '../util/BlurView'
|
import {BlurView} from '../util/BlurView'
|
||||||
import * as Toast from '../util/Toast'
|
import * as Toast from '../util/Toast'
|
||||||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
|
@ -28,14 +27,11 @@ import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {UserBanner} from '../util/UserBanner'
|
import {UserBanner} from '../util/UserBanner'
|
||||||
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
|
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
|
||||||
import {formatCount} from '../util/numeric/format'
|
import {formatCount} from '../util/numeric/format'
|
||||||
import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
|
|
||||||
import {Link} from '../util/Link'
|
import {Link} from '../util/Link'
|
||||||
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
|
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
|
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
|
||||||
import {
|
import {
|
||||||
RQKEY as profileQueryKey,
|
|
||||||
useProfileMuteMutationQueue,
|
|
||||||
useProfileBlockMutationQueue,
|
useProfileBlockMutationQueue,
|
||||||
useProfileFollowMutationQueue,
|
useProfileFollowMutationQueue,
|
||||||
} from '#/state/queries/profile'
|
} from '#/state/queries/profile'
|
||||||
|
@ -46,9 +42,7 @@ import {BACK_HITSLOP} from 'lib/constants'
|
||||||
import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles'
|
import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles'
|
||||||
import {makeProfileLink} from 'lib/routes/links'
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
import {pluralize} from 'lib/strings/helpers'
|
import {pluralize} from 'lib/strings/helpers'
|
||||||
import {toShareUrl} from 'lib/strings/url-helpers'
|
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||||
import {shareUrl} from 'lib/sharing'
|
|
||||||
import {s, colors} from 'lib/styles'
|
import {s, colors} from 'lib/styles'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
@ -57,6 +51,7 @@ import {useRequireAuth} from '#/state/session'
|
||||||
import {LabelInfo} from '../util/moderation/LabelInfo'
|
import {LabelInfo} from '../util/moderation/LabelInfo'
|
||||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
import {ProfileMenu} from 'view/com/profile/ProfileMenu'
|
||||||
|
|
||||||
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
|
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
@ -108,20 +103,12 @@ let ProfileHeader = ({
|
||||||
const {isDesktop} = useWebMediaQueries()
|
const {isDesktop} = useWebMediaQueries()
|
||||||
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
|
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
|
||||||
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
||||||
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
const [__, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const moderation = useMemo(
|
const moderation = useMemo(
|
||||||
() => moderateProfile(profile, moderationOpts),
|
() => moderateProfile(profile, moderationOpts),
|
||||||
[profile, moderationOpts],
|
[profile, moderationOpts],
|
||||||
)
|
)
|
||||||
|
|
||||||
const invalidateProfileQuery = React.useCallback(() => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: profileQueryKey(profile.did),
|
|
||||||
})
|
|
||||||
}, [queryClient, profile.did])
|
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
const onPressBack = React.useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
|
@ -189,72 +176,7 @@ let ProfileHeader = ({
|
||||||
})
|
})
|
||||||
}, [track, openModal, profile])
|
}, [track, openModal, profile])
|
||||||
|
|
||||||
const onPressShare = React.useCallback(() => {
|
const onPressUnblockAccount = React.useCallback(() => {
|
||||||
track('ProfileHeader:ShareButtonClicked')
|
|
||||||
shareUrl(toShareUrl(makeProfileLink(profile)))
|
|
||||||
}, [track, profile])
|
|
||||||
|
|
||||||
const onPressAddRemoveLists = React.useCallback(() => {
|
|
||||||
track('ProfileHeader:AddToListsButtonClicked')
|
|
||||||
openModal({
|
|
||||||
name: 'user-add-remove-lists',
|
|
||||||
subject: profile.did,
|
|
||||||
handle: profile.handle,
|
|
||||||
displayName: profile.displayName || profile.handle,
|
|
||||||
onAdd: invalidateProfileQuery,
|
|
||||||
onRemove: invalidateProfileQuery,
|
|
||||||
})
|
|
||||||
}, [track, profile, openModal, invalidateProfileQuery])
|
|
||||||
|
|
||||||
const onPressMuteAccount = React.useCallback(async () => {
|
|
||||||
track('ProfileHeader:MuteAccountButtonClicked')
|
|
||||||
try {
|
|
||||||
await queueMute()
|
|
||||||
Toast.show(_(msg`Account muted`))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.name !== 'AbortError') {
|
|
||||||
logger.error('Failed to mute account', {message: e})
|
|
||||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [track, queueMute, _])
|
|
||||||
|
|
||||||
const onPressUnmuteAccount = React.useCallback(async () => {
|
|
||||||
track('ProfileHeader:UnmuteAccountButtonClicked')
|
|
||||||
try {
|
|
||||||
await queueUnmute()
|
|
||||||
Toast.show(_(msg`Account unmuted`))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.name !== 'AbortError') {
|
|
||||||
logger.error('Failed to unmute account', {message: e})
|
|
||||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [track, queueUnmute, _])
|
|
||||||
|
|
||||||
const onPressBlockAccount = React.useCallback(async () => {
|
|
||||||
track('ProfileHeader:BlockAccountButtonClicked')
|
|
||||||
openModal({
|
|
||||||
name: 'confirm',
|
|
||||||
title: _(msg`Block Account`),
|
|
||||||
message: _(
|
|
||||||
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
|
||||||
),
|
|
||||||
onPressConfirm: async () => {
|
|
||||||
try {
|
|
||||||
await queueBlock()
|
|
||||||
Toast.show(_(msg`Account blocked`))
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.name !== 'AbortError') {
|
|
||||||
logger.error('Failed to block account', {message: e})
|
|
||||||
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [track, queueBlock, openModal, _])
|
|
||||||
|
|
||||||
const onPressUnblockAccount = React.useCallback(async () => {
|
|
||||||
track('ProfileHeader:UnblockAccountButtonClicked')
|
track('ProfileHeader:UnblockAccountButtonClicked')
|
||||||
openModal({
|
openModal({
|
||||||
name: 'confirm',
|
name: 'confirm',
|
||||||
|
@ -274,119 +196,12 @@ let ProfileHeader = ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, [track, queueUnblock, openModal, _])
|
}, [_, openModal, queueUnblock, track])
|
||||||
|
|
||||||
const onPressReportAccount = React.useCallback(() => {
|
|
||||||
track('ProfileHeader:ReportAccountButtonClicked')
|
|
||||||
openModal({
|
|
||||||
name: 'report',
|
|
||||||
did: profile.did,
|
|
||||||
})
|
|
||||||
}, [track, openModal, profile])
|
|
||||||
|
|
||||||
const isMe = React.useMemo(
|
const isMe = React.useMemo(
|
||||||
() => currentAccount?.did === profile.did,
|
() => currentAccount?.did === profile.did,
|
||||||
[currentAccount, profile],
|
[currentAccount, profile],
|
||||||
)
|
)
|
||||||
const dropdownItems: DropdownItem[] = React.useMemo(() => {
|
|
||||||
let items: DropdownItem[] = [
|
|
||||||
{
|
|
||||||
testID: 'profileHeaderDropdownShareBtn',
|
|
||||||
label: isWeb ? _(msg`Copy link to profile`) : _(msg`Share`),
|
|
||||||
onPress: onPressShare,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'square.and.arrow.up',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_share',
|
|
||||||
web: 'share',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
if (hasSession) {
|
|
||||||
items.push({label: 'separator'})
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownListAddRemoveBtn',
|
|
||||||
label: _(msg`Add to Lists`),
|
|
||||||
onPress: onPressAddRemoveLists,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'list.bullet',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_add',
|
|
||||||
web: 'list',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (!isMe) {
|
|
||||||
if (!profile.viewer?.blocking) {
|
|
||||||
if (!profile.viewer?.mutedByList) {
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownMuteBtn',
|
|
||||||
label: profile.viewer?.muted
|
|
||||||
? _(msg`Unmute Account`)
|
|
||||||
: _(msg`Mute Account`),
|
|
||||||
onPress: profile.viewer?.muted
|
|
||||||
? onPressUnmuteAccount
|
|
||||||
: onPressMuteAccount,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'speaker.slash',
|
|
||||||
},
|
|
||||||
android: 'ic_lock_silent_mode',
|
|
||||||
web: 'comment-slash',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!profile.viewer?.blockingByList) {
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownBlockBtn',
|
|
||||||
label: profile.viewer?.blocking
|
|
||||||
? _(msg`Unblock Account`)
|
|
||||||
: _(msg`Block Account`),
|
|
||||||
onPress: profile.viewer?.blocking
|
|
||||||
? onPressUnblockAccount
|
|
||||||
: onPressBlockAccount,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'person.fill.xmark',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_close_clear_cancel',
|
|
||||||
web: 'user-slash',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
items.push({
|
|
||||||
testID: 'profileHeaderDropdownReportBtn',
|
|
||||||
label: _(msg`Report Account`),
|
|
||||||
onPress: onPressReportAccount,
|
|
||||||
icon: {
|
|
||||||
ios: {
|
|
||||||
name: 'exclamationmark.triangle',
|
|
||||||
},
|
|
||||||
android: 'ic_menu_report_image',
|
|
||||||
web: 'circle-exclamation',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}, [
|
|
||||||
isMe,
|
|
||||||
hasSession,
|
|
||||||
profile.viewer?.muted,
|
|
||||||
profile.viewer?.mutedByList,
|
|
||||||
profile.viewer?.blocking,
|
|
||||||
profile.viewer?.blockingByList,
|
|
||||||
onPressShare,
|
|
||||||
onPressUnmuteAccount,
|
|
||||||
onPressMuteAccount,
|
|
||||||
onPressUnblockAccount,
|
|
||||||
onPressBlockAccount,
|
|
||||||
onPressReportAccount,
|
|
||||||
onPressAddRemoveLists,
|
|
||||||
_,
|
|
||||||
])
|
|
||||||
|
|
||||||
const blockHide =
|
const blockHide =
|
||||||
!isMe && (profile.viewer?.blocking || profile.viewer?.blockedBy)
|
!isMe && (profile.viewer?.blocking || profile.viewer?.blockedBy)
|
||||||
|
@ -516,17 +331,7 @@ let ProfileHeader = ({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{dropdownItems?.length ? (
|
<ProfileMenu profile={profile} />
|
||||||
<NativeDropdown
|
|
||||||
testID="profileHeaderDropdownBtn"
|
|
||||||
items={dropdownItems}
|
|
||||||
accessibilityLabel={_(msg`More options`)}
|
|
||||||
accessibilityHint="">
|
|
||||||
<View style={[styles.btn, styles.secondaryBtn, pal.btn]}>
|
|
||||||
<FontAwesomeIcon icon="ellipsis" size={20} style={[pal.text]} />
|
|
||||||
</View>
|
|
||||||
</NativeDropdown>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
</View>
|
||||||
<View pointerEvents="none">
|
<View pointerEvents="none">
|
||||||
<Text
|
<Text
|
||||||
|
|
|
@ -0,0 +1,307 @@
|
||||||
|
import React, {memo} from 'react'
|
||||||
|
import {TouchableOpacity} from 'react-native'
|
||||||
|
import {AppBskyActorDefs} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
import * as Toast from 'view/com/util/Toast'
|
||||||
|
import {EventStopper} from 'view/com/util/EventStopper'
|
||||||
|
import {useSession} from 'state/session'
|
||||||
|
import * as Menu from '#/components/Menu'
|
||||||
|
import {useTheme} from '#/alf'
|
||||||
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
|
import {shareUrl} from 'lib/sharing'
|
||||||
|
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||||
|
import {makeProfileLink} from 'lib/routes/links'
|
||||||
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
|
import {useModalControls} from 'state/modals'
|
||||||
|
import {
|
||||||
|
RQKEY as profileQueryKey,
|
||||||
|
useProfileBlockMutationQueue,
|
||||||
|
useProfileFollowMutationQueue,
|
||||||
|
useProfileMuteMutationQueue,
|
||||||
|
} from 'state/queries/profile'
|
||||||
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||||
|
import {ListSparkle_Stroke2_Corner0_Rounded as List} from '#/components/icons/ListSparkle'
|
||||||
|
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||||
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
|
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
||||||
|
import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
|
||||||
|
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
|
||||||
|
import {PeopleRemove2_Stroke2_Corner0_Rounded as UserMinus} from '#/components/icons/PeopleRemove2'
|
||||||
|
import {logger} from '#/logger'
|
||||||
|
import {Shadow} from 'state/cache/types'
|
||||||
|
|
||||||
|
let ProfileMenu = ({
|
||||||
|
profile,
|
||||||
|
}: {
|
||||||
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
|
}): React.ReactNode => {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const {currentAccount, hasSession} = useSession()
|
||||||
|
const t = useTheme()
|
||||||
|
// TODO ALF this
|
||||||
|
const pal = usePalette('default')
|
||||||
|
const {track} = useAnalytics()
|
||||||
|
const {openModal} = useModalControls()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const isSelf = currentAccount?.did === profile.did
|
||||||
|
|
||||||
|
const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
|
||||||
|
const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
|
||||||
|
const [, queueUnfollow] = useProfileFollowMutationQueue(profile)
|
||||||
|
|
||||||
|
const invalidateProfileQuery = React.useCallback(() => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: profileQueryKey(profile.did),
|
||||||
|
})
|
||||||
|
}, [queryClient, profile.did])
|
||||||
|
|
||||||
|
const onPressShare = React.useCallback(() => {
|
||||||
|
track('ProfileHeader:ShareButtonClicked')
|
||||||
|
shareUrl(toShareUrl(makeProfileLink(profile)))
|
||||||
|
}, [track, profile])
|
||||||
|
|
||||||
|
const onPressAddRemoveLists = React.useCallback(() => {
|
||||||
|
track('ProfileHeader:AddToListsButtonClicked')
|
||||||
|
openModal({
|
||||||
|
name: 'user-add-remove-lists',
|
||||||
|
subject: profile.did,
|
||||||
|
handle: profile.handle,
|
||||||
|
displayName: profile.displayName || profile.handle,
|
||||||
|
onAdd: invalidateProfileQuery,
|
||||||
|
onRemove: invalidateProfileQuery,
|
||||||
|
})
|
||||||
|
}, [track, profile, openModal, invalidateProfileQuery])
|
||||||
|
|
||||||
|
const onPressMuteAccount = React.useCallback(async () => {
|
||||||
|
if (profile.viewer?.muted) {
|
||||||
|
track('ProfileHeader:UnmuteAccountButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueUnmute()
|
||||||
|
Toast.show(_(msg`Account unmuted`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to unmute account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
track('ProfileHeader:MuteAccountButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueMute()
|
||||||
|
Toast.show(_(msg`Account muted`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to mute account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [profile.viewer?.muted, track, queueUnmute, _, queueMute])
|
||||||
|
|
||||||
|
const onPressBlockAccount = React.useCallback(async () => {
|
||||||
|
if (profile.viewer?.blocking) {
|
||||||
|
track('ProfileHeader:UnblockAccountButtonClicked')
|
||||||
|
openModal({
|
||||||
|
name: 'confirm',
|
||||||
|
title: _(msg`Unblock Account`),
|
||||||
|
message: _(
|
||||||
|
msg`The account will be able to interact with you after unblocking.`,
|
||||||
|
),
|
||||||
|
onPressConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await queueUnblock()
|
||||||
|
Toast.show(_(msg`Account unblocked`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to unblock account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
track('ProfileHeader:BlockAccountButtonClicked')
|
||||||
|
openModal({
|
||||||
|
name: 'confirm',
|
||||||
|
title: _(msg`Block Account`),
|
||||||
|
message: _(
|
||||||
|
msg`Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
|
||||||
|
),
|
||||||
|
onPressConfirm: async () => {
|
||||||
|
try {
|
||||||
|
await queueBlock()
|
||||||
|
Toast.show(_(msg`Account blocked`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to block account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [profile.viewer?.blocking, track, openModal, _, queueUnblock, queueBlock])
|
||||||
|
|
||||||
|
const onPressUnfollowAccount = React.useCallback(async () => {
|
||||||
|
track('ProfileHeader:UnfollowButtonClicked')
|
||||||
|
try {
|
||||||
|
await queueUnfollow()
|
||||||
|
Toast.show(_(msg`Account unfollowed`))
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e?.name !== 'AbortError') {
|
||||||
|
logger.error('Failed to unfollow account', {message: e})
|
||||||
|
Toast.show(_(msg`There was an issue! ${e.toString()}`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [_, queueUnfollow, track])
|
||||||
|
|
||||||
|
const onPressReportAccount = React.useCallback(() => {
|
||||||
|
track('ProfileHeader:ReportAccountButtonClicked')
|
||||||
|
openModal({
|
||||||
|
name: 'report',
|
||||||
|
did: profile.did,
|
||||||
|
})
|
||||||
|
}, [track, openModal, profile])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EventStopper onKeyDown={false}>
|
||||||
|
<Menu.Root>
|
||||||
|
<Menu.Trigger label={_(`More options`)}>
|
||||||
|
{({props}) => {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
{...props}
|
||||||
|
hitSlop={HITSLOP_10}
|
||||||
|
testID="profileHeaderDropdownBtn"
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 7,
|
||||||
|
borderRadius: 50,
|
||||||
|
marginLeft: 6,
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
},
|
||||||
|
pal.btn,
|
||||||
|
]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="ellipsis"
|
||||||
|
size={20}
|
||||||
|
style={t.atoms.text}
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Menu.Trigger>
|
||||||
|
|
||||||
|
<Menu.Outer style={{minWidth: 170}}>
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownShareBtn"
|
||||||
|
label={_(msg`Share`)}
|
||||||
|
onPress={onPressShare}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Share</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Share} />
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Group>
|
||||||
|
{hasSession && (
|
||||||
|
<>
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Group>
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownListAddRemoveBtn"
|
||||||
|
label={_(msg`Add to Lists`)}
|
||||||
|
onPress={onPressAddRemoveLists}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Add to Lists</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={List} />
|
||||||
|
</Menu.Item>
|
||||||
|
{!isSelf && (
|
||||||
|
<>
|
||||||
|
{profile.viewer?.following &&
|
||||||
|
(profile.viewer.blocking || profile.viewer.blockedBy) && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownUnfollowBtn"
|
||||||
|
label={_(msg`Unfollow Account`)}
|
||||||
|
onPress={onPressUnfollowAccount}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Unfollow Account</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={UserMinus} />
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{!profile.viewer?.blocking &&
|
||||||
|
!profile.viewer?.mutedByList && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownMuteBtn"
|
||||||
|
label={
|
||||||
|
profile.viewer?.muted
|
||||||
|
? _(msg`Unmute Account`)
|
||||||
|
: _(msg`Mute Account`)
|
||||||
|
}
|
||||||
|
onPress={onPressMuteAccount}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{profile.viewer?.muted ? (
|
||||||
|
<Trans>Unmute Account</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Mute Account</Trans>
|
||||||
|
)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon
|
||||||
|
icon={profile.viewer?.muted ? Unmute : Mute}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{!profile.viewer?.blockingByList && (
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownBlockBtn"
|
||||||
|
label={
|
||||||
|
profile.viewer
|
||||||
|
? _(msg`Unblock Account`)
|
||||||
|
: _(msg`Block Account`)
|
||||||
|
}
|
||||||
|
onPress={onPressBlockAccount}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
{profile.viewer?.blocking ? (
|
||||||
|
<Trans>Unblock Account</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Block Account</Trans>
|
||||||
|
)}
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon
|
||||||
|
icon={
|
||||||
|
profile.viewer?.blocking ? PersonCheck : PersonX
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
<Menu.Item
|
||||||
|
testID="profileHeaderDropdownReportBtn"
|
||||||
|
label={_(msg`Report Account`)}
|
||||||
|
onPress={onPressReportAccount}>
|
||||||
|
<Menu.ItemText>
|
||||||
|
<Trans>Report Account</Trans>
|
||||||
|
</Menu.ItemText>
|
||||||
|
<Menu.ItemIcon icon={Flag} />
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu.Outer>
|
||||||
|
</Menu.Root>
|
||||||
|
</EventStopper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ProfileMenu = memo(ProfileMenu)
|
||||||
|
export {ProfileMenu}
|
Loading…
Reference in New Issue