diff --git a/assets/icons/flag_stroke2_corner0_rounded.svg b/assets/icons/flag_stroke2_corner0_rounded.svg
new file mode 100644
index 00000000..9f9cc5cd
--- /dev/null
+++ b/assets/icons/flag_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg b/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg
new file mode 100644
index 00000000..2e798cbe
--- /dev/null
+++ b/assets/icons/peopleRemove2_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/personCheck_stroke2_corner0_rounded.svg b/assets/icons/personCheck_stroke2_corner0_rounded.svg
new file mode 100644
index 00000000..b3231c27
--- /dev/null
+++ b/assets/icons/personCheck_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/personX_stroke2_corner0_rounded.svg b/assets/icons/personX_stroke2_corner0_rounded.svg
new file mode 100644
index 00000000..073015bc
--- /dev/null
+++ b/assets/icons/personX_stroke2_corner0_rounded.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx
index f9b697ea..9be9dd86 100644
--- a/src/components/Menu/index.tsx
+++ b/src/components/Menu/index.tsx
@@ -1,5 +1,5 @@
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 {atoms as a, useTheme} from '#/alf'
@@ -75,7 +75,10 @@ export function Trigger({children, label}: TriggerProps) {
export function Outer({
children,
showCancel,
-}: React.PropsWithChildren<{showCancel?: boolean}>) {
+}: React.PropsWithChildren<{
+ showCancel?: boolean
+ style?: StyleProp
+}>) {
const context = React.useContext(Context)
return (
diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx
index f23c39ce..2004ee7c 100644
--- a/src/components/Menu/index.web.tsx
+++ b/src/components/Menu/index.web.tsx
@@ -1,7 +1,7 @@
/* eslint-disable react/prop-types */
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 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
+}>) {
const t = useTheme()
return (
@@ -144,6 +150,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) {
a.p_xs,
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
t.atoms.shadow_md,
+ style,
]}>
{children}
diff --git a/src/components/icons/Flag.tsx b/src/components/icons/Flag.tsx
new file mode 100644
index 00000000..d986db75
--- /dev/null
+++ b/src/components/icons/Flag.tsx
@@ -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',
+})
diff --git a/src/components/icons/PeopleRemove2.tsx b/src/components/icons/PeopleRemove2.tsx
new file mode 100644
index 00000000..3d16ed96
--- /dev/null
+++ b/src/components/icons/PeopleRemove2.tsx
@@ -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',
+})
diff --git a/src/components/icons/PersonCheck.tsx b/src/components/icons/PersonCheck.tsx
new file mode 100644
index 00000000..097271d8
--- /dev/null
+++ b/src/components/icons/PersonCheck.tsx
@@ -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',
+})
diff --git a/src/components/icons/PersonX.tsx b/src/components/icons/PersonX.tsx
new file mode 100644
index 00000000..a015e137
--- /dev/null
+++ b/src/components/icons/PersonX.tsx
@@ -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',
+})
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 3e479d7b..a11fe837 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -7,7 +7,6 @@ import {
} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native'
-import {useQueryClient} from '@tanstack/react-query'
import {
AppBskyActorDefs,
ModerationOpts,
@@ -17,7 +16,7 @@ import {
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {NavigationProp} from 'lib/routes/types'
-import {isNative, isWeb} from 'platform/detection'
+import {isNative} from 'platform/detection'
import {BlurView} from '../util/BlurView'
import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
@@ -28,14 +27,11 @@ import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner'
import {ProfileHeaderAlerts} from '../util/moderation/ProfileHeaderAlerts'
import {formatCount} from '../util/numeric/format'
-import {NativeDropdown, DropdownItem} from '../util/forms/NativeDropdown'
import {Link} from '../util/Link'
import {ProfileHeaderSuggestedFollows} from './ProfileHeaderSuggestedFollows'
import {useModalControls} from '#/state/modals'
import {useLightboxControls, ProfileImageLightbox} from '#/state/lightbox'
import {
- RQKEY as profileQueryKey,
- useProfileMuteMutationQueue,
useProfileBlockMutationQueue,
useProfileFollowMutationQueue,
} from '#/state/queries/profile'
@@ -46,9 +42,7 @@ import {BACK_HITSLOP} from 'lib/constants'
import {isInvalidHandle, sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {pluralize} from 'lib/strings/helpers'
-import {toShareUrl} from 'lib/strings/url-helpers'
import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {shareUrl} from 'lib/sharing'
import {s, colors} from 'lib/styles'
import {logger} from '#/logger'
import {useSession} from '#/state/session'
@@ -57,6 +51,7 @@ import {useRequireAuth} from '#/state/session'
import {LabelInfo} from '../util/moderation/LabelInfo'
import {useProfileShadow} from 'state/cache/profile-shadow'
import {atoms as a} from '#/alf'
+import {ProfileMenu} from 'view/com/profile/ProfileMenu'
let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
const pal = usePalette('default')
@@ -108,20 +103,12 @@ let ProfileHeader = ({
const {isDesktop} = useWebMediaQueries()
const [showSuggestedFollows, setShowSuggestedFollows] = React.useState(false)
const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue(profile)
- const [queueMute, queueUnmute] = useProfileMuteMutationQueue(profile)
- const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile)
- const queryClient = useQueryClient()
+ const [__, queueUnblock] = useProfileBlockMutationQueue(profile)
const moderation = useMemo(
() => moderateProfile(profile, moderationOpts),
[profile, moderationOpts],
)
- const invalidateProfileQuery = React.useCallback(() => {
- queryClient.invalidateQueries({
- queryKey: profileQueryKey(profile.did),
- })
- }, [queryClient, profile.did])
-
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {
navigation.goBack()
@@ -189,72 +176,7 @@ let ProfileHeader = ({
})
}, [track, openModal, profile])
- 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 () => {
- 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 () => {
+ const onPressUnblockAccount = React.useCallback(() => {
track('ProfileHeader:UnblockAccountButtonClicked')
openModal({
name: 'confirm',
@@ -274,119 +196,12 @@ let ProfileHeader = ({
}
},
})
- }, [track, queueUnblock, openModal, _])
-
- const onPressReportAccount = React.useCallback(() => {
- track('ProfileHeader:ReportAccountButtonClicked')
- openModal({
- name: 'report',
- did: profile.did,
- })
- }, [track, openModal, profile])
+ }, [_, openModal, queueUnblock, track])
const isMe = React.useMemo(
() => currentAccount?.did === profile.did,
[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 =
!isMe && (profile.viewer?.blocking || profile.viewer?.blockedBy)
@@ -516,17 +331,7 @@ let ProfileHeader = ({
)}
>
) : null}
- {dropdownItems?.length ? (
-
-
-
-
-
- ) : undefined}
+
+}): 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 (
+
+
+
+ {({props}) => {
+ return (
+
+
+
+ )
+ }}
+
+
+
+
+
+
+ Share
+
+
+
+
+ {hasSession && (
+ <>
+
+
+
+
+ Add to Lists
+
+
+
+ {!isSelf && (
+ <>
+ {profile.viewer?.following &&
+ (profile.viewer.blocking || profile.viewer.blockedBy) && (
+
+
+ Unfollow Account
+
+
+
+ )}
+ {!profile.viewer?.blocking &&
+ !profile.viewer?.mutedByList && (
+
+
+ {profile.viewer?.muted ? (
+ Unmute Account
+ ) : (
+ Mute Account
+ )}
+
+
+
+ )}
+ {!profile.viewer?.blockingByList && (
+
+
+ {profile.viewer?.blocking ? (
+ Unblock Account
+ ) : (
+ Block Account
+ )}
+
+
+
+ )}
+
+
+ Report Account
+
+
+
+ >
+ )}
+
+ >
+ )}
+
+
+
+ )
+}
+
+ProfileMenu = memo(ProfileMenu)
+export {ProfileMenu}