Combine actions, convert to new menu (#3174)

* Combine actions, convert to new menu

* remove about tab and move content to header

* Tweak alignment

* fix missing rkey

* hog the like button

* Add a little more whitespace

* Improve a11y

* Yeah toast

* Update usage

* Pin to Home

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
zio/stable
Eric Bailey 2024-03-12 13:50:53 -05:00 committed by GitHub
parent 8123299192
commit c9d821c572
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 182 additions and 202 deletions

View File

@ -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="M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 245 B

View File

@ -228,6 +228,7 @@ export function InlineLink({
onPress: outerOnPress, onPress: outerOnPress,
download, download,
selectable, selectable,
label,
...rest ...rest
}: InlineLinkProps) { }: InlineLinkProps) {
const t = useTheme() const t = useTheme()
@ -255,7 +256,8 @@ export function InlineLink({
return ( return (
<Text <Text
selectable={selectable} selectable={selectable}
label={href} accessibilityHint=""
accessibilityLabel={label || href}
{...rest} {...rest}
style={[ style={[
{color: t.palette.primary_500}, {color: t.palette.primary_500},

View File

@ -0,0 +1,5 @@
import {createSinglePathSVG} from './TEMPLATE'
export const DotGrid_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M2 12a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm16 0a2 2 0 1 1 4 0 2 2 0 0 1-4 0Zm-6-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z',
})

View File

@ -0,0 +1,9 @@
import {createSinglePathSVG} from './TEMPLATE'
export const Heart2_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M16.734 5.091c-1.238-.276-2.708.047-4.022 1.38a1 1 0 0 1-1.424 0C9.974 5.137 8.504 4.814 7.266 5.09c-1.263.282-2.379 1.206-2.92 2.556C3.33 10.18 4.252 14.84 12 19.348c7.747-4.508 8.67-9.168 7.654-11.7-.541-1.351-1.657-2.275-2.92-2.557Zm4.777 1.812c1.604 4-.494 9.69-9.022 14.47a1 1 0 0 1-.978 0C2.983 16.592.885 10.902 2.49 6.902c.779-1.942 2.414-3.334 4.342-3.764 1.697-.378 3.552.003 5.169 1.286 1.617-1.283 3.472-1.664 5.17-1.286 1.927.43 3.562 1.822 4.34 3.764Z',
})
export const Heart2_Filled_Stroke2_Corner0_Rounded = createSinglePathSVG({
path: 'M12.489 21.372c8.528-4.78 10.626-10.47 9.022-14.47-.779-1.941-2.414-3.333-4.342-3.763-1.697-.378-3.552.003-5.169 1.287-1.617-1.284-3.472-1.665-5.17-1.287-1.927.43-3.562 1.822-4.34 3.764-1.605 4 .493 9.69 9.021 14.47a1 1 0 0 0 .978 0Z',
})

View File

@ -1,11 +1,9 @@
import React, {useMemo, useCallback} from 'react' import React, {useMemo, useCallback} from 'react'
import {Dimensions, StyleSheet, View} from 'react-native' import {StyleSheet, View, Pressable} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useIsFocused, useNavigation} from '@react-navigation/native' import {useIsFocused, useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {HeartIcon, HeartIconSolid} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from 'lib/routes/types'
import {makeRecordUri} from 'lib/strings/url-helpers' import {makeRecordUri} from 'lib/strings/url-helpers'
import {s} from 'lib/styles' import {s} from 'lib/styles'
@ -13,7 +11,7 @@ import {FeedDescriptor} from '#/state/queries/post-feed'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader' import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
import {Feed} from 'view/com/posts/Feed' import {Feed} from 'view/com/posts/Feed'
import {TextLink} from 'view/com/util/Link' import {InlineLink} from '#/components/Link'
import {ListRef} from 'view/com/util/List' import {ListRef} from 'view/com/util/List'
import {Button} from 'view/com/util/forms/Button' import {Button} from 'view/com/util/forms/Button'
import {Text} from 'view/com/util/text/Text' import {Text} from 'view/com/util/text/Text'
@ -29,15 +27,10 @@ import {shareUrl} from 'lib/sharing'
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {Haptics} from 'lib/haptics' import {Haptics} from 'lib/haptics'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {NativeDropdown, DropdownItem} from 'view/com/util/forms/NativeDropdown'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {makeCustomFeedLink} from 'lib/routes/links' import {makeCustomFeedLink} from 'lib/routes/links'
import {pluralize} from 'lib/strings/helpers' import {pluralize} from 'lib/strings/helpers'
import {CenteredView, ScrollView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
import {sanitizeHandle} from 'lib/strings/handles'
import {makeProfileLink} from 'lib/routes/links'
import {ComposeIcon2} from 'lib/icons' import {ComposeIcon2} from 'lib/icons'
import {logger} from '#/logger' import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
@ -59,9 +52,21 @@ import {useComposerControls} from '#/state/shell/composer'
import {truncateAndInvalidate} from '#/state/queries/util' import {truncateAndInvalidate} from '#/state/queries/util'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events' import {listenSoftReset} from '#/state/events'
import {atoms as a} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as Menu from '#/components/Menu'
import {HITSLOP_20} from '#/lib/constants'
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
import {
Heart2_Stroke2_Corner0_Rounded as HeartOutline,
Heart2_Filled_Stroke2_Corner0_Rounded as HeartFilled,
} from '#/components/icons/Heart2'
import {Button as NewButton, ButtonText} from '#/components/Button'
const SECTION_TITLES = ['Posts', 'About'] const SECTION_TITLES = ['Posts']
interface SectionRef { interface SectionRef {
scrollToTop: () => void scrollToTop: () => void
@ -148,7 +153,7 @@ export function ProfileFeedScreenInner({
feedInfo: FeedSourceFeedInfo feedInfo: FeedSourceFeedInfo
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const pal = usePalette('default') const t = useTheme()
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
@ -200,9 +205,11 @@ export function ProfileFeedScreenInner({
if (isSaved) { if (isSaved) {
await removeFeed({uri: feedInfo.uri}) await removeFeed({uri: feedInfo.uri})
resetRemoveFeed() resetRemoveFeed()
Toast.show(_(msg`Removed from your feeds`))
} else { } else {
await saveFeed({uri: feedInfo.uri}) await saveFeed({uri: feedInfo.uri})
resetSaveFeed() resetSaveFeed()
Toast.show(_(msg`Saved to your feeds`))
} }
} catch (err) { } catch (err) {
Toast.show( Toast.show(
@ -263,130 +270,132 @@ export function ProfileFeedScreenInner({
[feedSectionRef], [feedSectionRef],
) )
// render
// =
const dropdownItems: DropdownItem[] = React.useMemo(() => {
return [
hasSession && {
testID: 'feedHeaderDropdownToggleSavedBtn',
label: isSaved ? _(msg`Remove from my feeds`) : _(msg`Add to my feeds`),
onPress: isSavePending || isRemovePending ? undefined : onToggleSaved,
icon: isSaved
? {
ios: {
name: 'trash',
},
android: 'ic_delete',
web: ['far', 'trash-can'],
}
: {
ios: {
name: 'plus',
},
android: '',
web: 'plus',
},
},
hasSession && {
testID: 'feedHeaderDropdownReportBtn',
label: _(msg`Report feed`),
onPress: onPressReport,
icon: {
ios: {
name: 'exclamationmark.triangle',
},
android: 'ic_menu_report_image',
web: 'circle-exclamation',
},
},
{
testID: 'feedHeaderDropdownShareBtn',
label: _(msg`Share feed`),
onPress: onPressShare,
icon: {
ios: {
name: 'square.and.arrow.up',
},
android: 'ic_menu_share',
web: 'share',
},
},
].filter(Boolean) as DropdownItem[]
}, [
hasSession,
onToggleSaved,
onPressReport,
onPressShare,
isSaved,
isSavePending,
isRemovePending,
_,
])
const renderHeader = useCallback(() => { const renderHeader = useCallback(() => {
return ( return (
<ProfileSubpageHeader <>
isLoading={false} <ProfileSubpageHeader
href={feedInfo.route.href} isLoading={false}
title={feedInfo?.displayName} href={feedInfo.route.href}
avatar={feedInfo?.avatar} title={feedInfo?.displayName}
isOwner={feedInfo.creatorDid === currentAccount?.did} avatar={feedInfo?.avatar}
creator={ isOwner={feedInfo.creatorDid === currentAccount?.did}
feedInfo creator={
? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle} feedInfo
: undefined ? {did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}
} : undefined
avatarType="algo"> }
{feedInfo && hasSession && ( avatarType="algo">
<> <View style={[a.flex_row, a.align_center, a.gap_sm]}>
<Button {feedInfo && hasSession && (
disabled={isSavePending || isRemovePending} <NewButton
type="default" testID={isPinned ? 'unpinBtn' : 'pinBtn'}
label={isSaved ? _(msg`Unsave`) : _(msg`Save`)} disabled={isPinPending || isUnpinPending}
onPress={onToggleSaved} size="small"
style={styles.btn} variant="solid"
/> color={isPinned ? 'secondary' : 'primary'}
<Button label={isPinned ? _(msg`Unpin from home`) : _(msg`Pin to home`)}
testID={isPinned ? 'unpinBtn' : 'pinBtn'} onPress={onTogglePinned}>
disabled={isPinPending || isUnpinPending} <ButtonText>
type={isPinned ? 'default' : 'inverted'} {isPinned ? _(msg`Unpin`) : _(msg`Pin to Home`)}
label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)} </ButtonText>
onPress={onTogglePinned} </NewButton>
style={styles.btn} )}
/> <Menu.Root>
</> <Menu.Trigger label={_(msg`Open feed options menu`)}>
)} {({props, state}) => {
<NativeDropdown return (
testID="headerDropdownBtn" <Pressable
items={dropdownItems} {...props}
accessibilityLabel={_(msg`More options`)} hitSlop={HITSLOP_20}
accessibilityHint=""> style={[
<View style={[pal.viewLight, styles.btn]}> a.justify_center,
<FontAwesomeIcon a.align_center,
icon="ellipsis" a.rounded_full,
size={20} {height: 36, width: 36},
color={pal.colors.text} t.atoms.bg_contrast_50,
/> (state.hovered || state.pressed) && [
t.atoms.bg_contrast_100,
],
]}
testID="headerDropdownBtn">
<Ellipsis
size="lg"
fill={t.atoms.text_contrast_medium.color}
/>
</Pressable>
)
}}
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
{hasSession && (
<>
<Menu.Item
disabled={isSavePending || isRemovePending}
testID="feedHeaderDropdownToggleSavedBtn"
label={
isSaved
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)
}
onPress={onToggleSaved}>
<Menu.ItemText>
{isSaved
? _(msg`Remove from my feeds`)
: _(msg`Save to my feeds`)}
</Menu.ItemText>
<Menu.ItemIcon
icon={isSaved ? Trash : Plus}
position="right"
/>
</Menu.Item>
<Menu.Item
testID="feedHeaderDropdownReportBtn"
label={_(msg`Report feed`)}
onPress={onPressReport}>
<Menu.ItemText>{_(msg`Report feed`)}</Menu.ItemText>
<Menu.ItemIcon icon={CircleInfo} position="right" />
</Menu.Item>
</>
)}
<Menu.Item
testID="feedHeaderDropdownShareBtn"
label={_(msg`Share feed`)}
onPress={onPressShare}>
<Menu.ItemText>{_(msg`Share feed`)}</Menu.ItemText>
<Menu.ItemIcon icon={Share} position="right" />
</Menu.Item>
</Menu.Group>
</Menu.Outer>
</Menu.Root>
</View> </View>
</NativeDropdown> </ProfileSubpageHeader>
</ProfileSubpageHeader> <AboutSection
feedOwnerDid={feedInfo.creatorDid}
feedRkey={feedInfo.route.params.rkey}
feedInfo={feedInfo}
/>
</>
) )
}, [ }, [
_, _,
hasSession, hasSession,
pal,
feedInfo, feedInfo,
isPinned, isPinned,
onTogglePinned, onTogglePinned,
onToggleSaved, onToggleSaved,
dropdownItems,
currentAccount?.did, currentAccount?.did,
isPinPending, isPinPending,
isRemovePending, isRemovePending,
isSavePending, isSavePending,
isSaved, isSaved,
isUnpinPending, isUnpinPending,
onPressReport,
onPressShare,
t,
]) ])
return ( return (
@ -405,18 +414,6 @@ export function ProfileFeedScreenInner({
isFocused={isScreenFocused && isFocused} isFocused={isScreenFocused && isFocused}
/> />
)} )}
{({headerHeight, scrollElRef}) => (
<AboutSection
feedOwnerDid={feedInfo.creatorDid}
feedRkey={feedInfo.route.params.rkey}
feedInfo={feedInfo}
headerHeight={headerHeight}
scrollElRef={
scrollElRef as React.MutableRefObject<ScrollView | null>
}
isOwner={feedInfo.creatorDid === currentAccount?.did}
/>
)}
</PagerWithHeader> </PagerWithHeader>
{hasSession && ( {hasSession && (
<FAB <FAB
@ -505,21 +502,14 @@ function AboutSection({
feedOwnerDid, feedOwnerDid,
feedRkey, feedRkey,
feedInfo, feedInfo,
headerHeight,
scrollElRef,
isOwner,
}: { }: {
feedOwnerDid: string feedOwnerDid: string
feedRkey: string feedRkey: string
feedInfo: FeedSourceFeedInfo feedInfo: FeedSourceFeedInfo
headerHeight: number
scrollElRef: React.MutableRefObject<ScrollView | null>
isOwner: boolean
}) { }) {
const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const scrollHandlers = useScrollHandlers()
const onScroll = useAnimatedScrollHandler(scrollHandlers)
const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri) const [likeUri, setLikeUri] = React.useState(feedInfo.likeUri)
const {hasSession} = useSession() const {hasSession} = useSession()
const {track} = useAnalytics() const {track} = useAnalytics()
@ -555,24 +545,8 @@ function AboutSection({
}, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _]) }, [likeUri, isLiked, feedInfo, likeFeed, unlikeFeed, track, _])
return ( return (
<ScrollView <View style={[styles.aboutSectionContainer]}>
ref={scrollElRef} <View style={[a.pt_sm]}>
onScroll={onScroll}
scrollEventThrottle={1}
contentContainerStyle={{
paddingTop: headerHeight,
minHeight: Dimensions.get('window').height * 1.5,
}}>
<View
style={[
{
borderTopWidth: 1,
paddingVertical: 20,
paddingHorizontal: 20,
gap: 12,
},
pal.border,
]}>
{feedInfo.description ? ( {feedInfo.description ? (
<RichText <RichText
testID="listDescription" testID="listDescription"
@ -584,50 +558,34 @@ function AboutSection({
<Trans>No description</Trans> <Trans>No description</Trans>
</Text> </Text>
)} )}
<View style={{flexDirection: 'row', alignItems: 'center', gap: 10}}>
<Button
type="default"
testID="toggleLikeBtn"
accessibilityLabel={_(msg`Like this feed`)}
accessibilityHint=""
disabled={!hasSession || isLikePending || isUnlikePending}
onPress={onToggleLiked}
style={{paddingHorizontal: 10}}>
{isLiked ? (
<HeartIconSolid size={19} style={s.likeColor} />
) : (
<HeartIcon strokeWidth={3} size={19} style={pal.textLight} />
)}
</Button>
{typeof likeCount === 'number' && (
<TextLink
href={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
text={_(
msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`,
)}
style={[pal.textLight, s.semiBold]}
/>
)}
</View>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
{isOwner ? (
<Trans>Created by you</Trans>
) : (
<Trans>
Created by{' '}
<TextLink
text={sanitizeHandle(feedInfo.creatorHandle, '@')}
href={makeProfileLink({
did: feedInfo.creatorDid,
handle: feedInfo.creatorHandle,
})}
style={pal.textLight}
/>
</Trans>
)}
</Text>
</View> </View>
</ScrollView>
<View style={[a.flex_row, a.gap_sm, a.align_center, a.pb_sm]}>
<NewButton
size="small"
variant="solid"
color="secondary"
shape="round"
label={isLiked ? _(msg`Unlike this feed`) : _(msg`Like this feed`)}
testID="toggleLikeBtn"
disabled={!hasSession || isLikePending || isUnlikePending}
onPress={onToggleLiked}>
{isLiked ? (
<HeartFilled size="md" fill={s.likeColor.color} />
) : (
<HeartOutline size="md" fill={t.atoms.text_contrast_medium.color} />
)}
</NewButton>
{typeof likeCount === 'number' && (
<InlineLink
label={_(msg`View users who like this feed`)}
to={makeCustomFeedLink(feedOwnerDid, feedRkey, 'liked-by')}
style={[t.atoms.text_contrast_medium, a.font_bold]}>
{_(msg`Liked by ${likeCount} ${pluralize(likeCount, 'user')}`)}
</InlineLink>
)}
</View>
</View>
) )
} }
@ -647,4 +605,9 @@ const styles = StyleSheet.create({
paddingVertical: 14, paddingVertical: 14,
borderRadius: 6, borderRadius: 6,
}, },
aboutSectionContainer: {
paddingVertical: 4,
paddingHorizontal: 16,
gap: 12,
},
}) })