Tweak ProfileList design (#7100)

* Remove "No description"

* Move Lists about into header

* Remove pager with one tab

* Layout tweaks
This commit is contained in:
dan
2024-12-13 21:38:43 +00:00
committed by GitHub
parent adbc27059a
commit da6bcc54cf
4 changed files with 181 additions and 210 deletions
@@ -407,6 +407,7 @@ function Header({
isOwner={isOwn} isOwner={isOwn}
avatar={undefined} avatar={undefined}
creator={creator} creator={creator}
purpose="app.bsky.graph.defs#referencelist"
avatarType="starter-pack"> avatarType="starter-pack">
{hasSession ? ( {hasSession ? (
<View style={[a.flex_row, a.gap_sm, a.align_center]}> <View style={[a.flex_row, a.gap_sm, a.align_center]}>
+3 -1
View File
@@ -21,6 +21,7 @@ import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
import {isIOS} from '#/platform/detection' import {isIOS} from '#/platform/detection'
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager' import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
import {useTheme} from '#/alf'
import {ListMethods} from '../util/List' import {ListMethods} from '../util/List'
import {PagerHeaderProvider} from './PagerHeaderContext' import {PagerHeaderProvider} from './PagerHeaderContext'
import {TabBar} from './TabBar' import {TabBar} from './TabBar'
@@ -256,6 +257,7 @@ let PagerTabBar = ({
dragProgress: SharedValue<number> dragProgress: SharedValue<number>
dragState: SharedValue<'idle' | 'dragging' | 'settling'> dragState: SharedValue<'idle' | 'dragging' | 'settling'>
}): React.ReactNode => { }): React.ReactNode => {
const t = useTheme()
const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0) const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
const headerTransform = useAnimatedStyle(() => { const headerTransform = useAnimatedStyle(() => {
const translateY = const translateY =
@@ -277,7 +279,7 @@ let PagerTabBar = ({
return ( return (
<Animated.View <Animated.View
pointerEvents={isIOS ? 'auto' : 'box-none'} pointerEvents={isIOS ? 'auto' : 'box-none'}
style={[styles.tabBarMobile, headerTransform]}> style={[styles.tabBarMobile, headerTransform, t.atoms.bg]}>
<View <View
ref={headerRef} ref={headerRef}
pointerEvents={isIOS ? 'auto' : 'box-none'} pointerEvents={isIOS ? 'auto' : 'box-none'}
+47 -18
View File
@@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {Pressable, View} from 'react-native' import {Pressable, View} from 'react-native'
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
import {AppBskyGraphDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@@ -26,6 +27,7 @@ export function ProfileSubpageHeader({
title, title,
avatar, avatar,
isOwner, isOwner,
purpose,
creator, creator,
avatarType, avatarType,
children, children,
@@ -35,6 +37,7 @@ export function ProfileSubpageHeader({
title: string | undefined title: string | undefined
avatar: string | undefined avatar: string | undefined
isOwner: boolean | undefined isOwner: boolean | undefined
purpose: AppBskyGraphDefs.ListPurpose | undefined
creator: creator:
| { | {
did: string did: string
@@ -105,7 +108,7 @@ export function ProfileSubpageHeader({
alignItems: 'flex-start', alignItems: 'flex-start',
gap: 10, gap: 10,
paddingTop: 14, paddingTop: 14,
paddingBottom: 6, paddingBottom: 14,
paddingHorizontal: isMobile ? 12 : 14, paddingHorizontal: isMobile ? 12 : 14,
}}> }}>
<View ref={aviRef} collapsable={false}> <View ref={aviRef} collapsable={false}>
@@ -123,7 +126,7 @@ export function ProfileSubpageHeader({
)} )}
</Pressable> </Pressable>
</View> </View>
<View style={{flex: 1}}> <View style={{flex: 1, gap: 4}}>
{isLoading ? ( {isLoading ? (
<LoadingPlaceholder <LoadingPlaceholder
width={200} width={200}
@@ -142,24 +145,50 @@ export function ProfileSubpageHeader({
/> />
)} )}
{isLoading ? ( {isLoading || !creator ? (
<LoadingPlaceholder width={50} height={8} /> <LoadingPlaceholder width={50} height={8} />
) : ( ) : (
<Text type="xl" style={[pal.textLight]} numberOfLines={1}> <Text type="lg" style={[pal.textLight]} numberOfLines={1}>
{!creator ? ( {purpose === 'app.bsky.graph.defs#curatelist' ? (
<Trans>by </Trans> isOwner ? (
) : isOwner ? ( <Trans>List by you</Trans>
<Trans>by you</Trans> ) : (
) : ( <Trans>
<Trans> List by{' '}
by{' '} <TextLink
<TextLink text={sanitizeHandle(creator.handle || '', '@')}
text={sanitizeHandle(creator.handle, '@')} href={makeProfileLink(creator)}
href={makeProfileLink(creator)} style={pal.textLight}
style={pal.textLight} />
/> </Trans>
</Trans> )
)} ) : purpose === 'app.bsky.graph.defs#modlist' ? (
isOwner ? (
<Trans>Moderation list by you</Trans>
) : (
<Trans>
Moderation list by{' '}
<TextLink
text={sanitizeHandle(creator.handle || '', '@')}
href={makeProfileLink(creator)}
style={pal.textLight}
/>
</Trans>
)
) : purpose === 'app.bsky.graph.defs#referencelist' ? (
isOwner ? (
<Trans>Starter pack by you</Trans>
) : (
<Trans>
Starter pack by{' '}
<TextLink
text={sanitizeHandle(creator.handle || '', '@')}
href={makeProfileLink(creator)}
style={pal.textLight}
/>
</Trans>
)
) : null}
</Text> </Text>
)} )}
</View> </View>
+130 -191
View File
@@ -1,5 +1,6 @@
import React, {useCallback, useMemo} from 'react' import React, {useCallback, useMemo} from 'react'
import {Pressable, StyleSheet, View} from 'react-native' import {Pressable, StyleSheet, View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated'
import { import {
AppBskyGraphDefs, AppBskyGraphDefs,
AtUri, AtUri,
@@ -19,12 +20,11 @@ import {usePalette} from '#/lib/hooks/usePalette'
import {useSetTitle} from '#/lib/hooks/useSetTitle' import {useSetTitle} from '#/lib/hooks/useSetTitle'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {ComposeIcon2} from '#/lib/icons' import {ComposeIcon2} from '#/lib/icons'
import {makeListLink, makeProfileLink} from '#/lib/routes/links' import {makeListLink} from '#/lib/routes/links'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types' import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {NavigationProp} from '#/lib/routes/types' import {NavigationProp} from '#/lib/routes/types'
import {shareUrl} from '#/lib/sharing' import {shareUrl} from '#/lib/sharing'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {toShareUrl} from '#/lib/strings/url-helpers' import {toShareUrl} from '#/lib/strings/url-helpers'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -63,14 +63,13 @@ import {
DropdownItem, DropdownItem,
NativeDropdown, NativeDropdown,
} from '#/view/com/util/forms/NativeDropdown' } from '#/view/com/util/forms/NativeDropdown'
import {TextLink} from '#/view/com/util/Link'
import {ListRef} from '#/view/com/util/List' import {ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {LoadingScreen} from '#/view/com/util/LoadingScreen' import {LoadingScreen} from '#/view/com/util/LoadingScreen'
import {Text} from '#/view/com/util/text/Text' import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
import {atoms as a, useTheme} from '#/alf' import {atoms as a} from '#/alf'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import * as Hider from '#/components/moderation/Hider' import * as Hider from '#/components/moderation/Hider'
@@ -78,8 +77,7 @@ import * as Prompt from '#/components/Prompt'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog' import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
const SECTION_TITLES_CURATE = ['Posts', 'About'] const SECTION_TITLES_CURATE = ['Posts', 'People']
const SECTION_TITLES_MOD = ['About']
interface SectionRef { interface SectionRef {
scrollToTop: () => void scrollToTop: () => void
@@ -161,6 +159,7 @@ function ProfileListScreenLoaded({
const isScreenFocused = useIsFocused() const isScreenFocused = useIsFocused()
const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1 const isHidden = list.labels?.findIndex(l => l.val === '!hide') !== -1
const isOwner = currentAccount?.did === list.creator.did const isOwner = currentAccount?.did === list.creator.did
const scrollElRef = useAnimatedRef()
const moderation = React.useMemo(() => { const moderation = React.useMemo(() => {
return moderateUserList(list, moderationOpts) return moderateUserList(list, moderationOpts)
@@ -259,19 +258,13 @@ function ProfileListScreenLoaded({
</Hider.Mask> </Hider.Mask>
<Hider.Content> <Hider.Content>
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
<PagerWithHeader <Layout.Center>{renderHeader()}</Layout.Center>
items={SECTION_TITLES_MOD} <AboutSection
isHeaderReady={true} list={list}
renderHeader={renderHeader}> scrollElRef={scrollElRef as ListRef}
{({headerHeight, scrollElRef}) => ( onPressAddUser={onPressAddUser}
<AboutSection headerHeight={0}
list={list} />
scrollElRef={scrollElRef as ListRef}
onPressAddUser={onPressAddUser}
headerHeight={headerHeight}
/>
)}
</PagerWithHeader>
<FAB <FAB
testID="composeFAB" testID="composeFAB"
onPress={() => openComposer({})} onPress={() => openComposer({})}
@@ -652,101 +645,124 @@ function Header({
] ]
}, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open]) }, [_, subscribeMutePromptControl.open, subscribeBlockPromptControl.open])
const descriptionRT = useMemo(
() =>
list.description
? new RichTextAPI({
text: list.description,
facets: list.descriptionFacets,
})
: undefined,
[list],
)
return ( return (
<ProfileSubpageHeader <>
href={makeListLink(list.creator.handle || list.creator.did || '', rkey)} <ProfileSubpageHeader
title={list.name} href={makeListLink(list.creator.handle || list.creator.did || '', rkey)}
avatar={list.avatar} title={list.name}
isOwner={list.creator.did === currentAccount?.did} avatar={list.avatar}
creator={list.creator} isOwner={list.creator.did === currentAccount?.did}
avatarType="list"> creator={list.creator}
<ReportDialog purpose={list.purpose}
control={reportDialogControl} avatarType="list">
params={{ <ReportDialog
type: 'list', control={reportDialogControl}
uri: list.uri, params={{
cid: list.cid, type: 'list',
}} uri: list.uri,
/> cid: list.cid,
{isCurateList ? ( }}
<Button
testID={isPinned ? 'unpinBtn' : 'pinBtn'}
type={isPinned ? 'default' : 'inverted'}
label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
onPress={onTogglePinned}
disabled={isPending}
/> />
) : isModList ? ( {isCurateList ? (
isBlocking ? (
<Button <Button
testID="unblockBtn" testID={isPinned ? 'unpinBtn' : 'pinBtn'}
type="default" type={isPinned ? 'default' : 'inverted'}
label={_(msg`Unblock`)} label={isPinned ? _(msg`Unpin`) : _(msg`Pin to home`)}
onPress={onUnsubscribeBlock} onPress={onTogglePinned}
disabled={isPending}
/> />
) : isMuting ? ( ) : isModList ? (
<Button isBlocking ? (
testID="unmuteBtn" <Button
type="default" testID="unblockBtn"
label={_(msg`Unmute`)} type="default"
onPress={onUnsubscribeMute} label={_(msg`Unblock`)}
/> onPress={onUnsubscribeBlock}
) : ( />
<NativeDropdown ) : isMuting ? (
testID="subscribeBtn" <Button
items={subscribeDropdownItems} testID="unmuteBtn"
accessibilityLabel={_(msg`Subscribe to this list`)} type="default"
accessibilityHint=""> label={_(msg`Unmute`)}
<View style={[palInverted.view, styles.btn]}> onPress={onUnsubscribeMute}
<Text style={palInverted.text}> />
<Trans>Subscribe</Trans> ) : (
</Text> <NativeDropdown
</View> testID="subscribeBtn"
</NativeDropdown> items={subscribeDropdownItems}
) accessibilityLabel={_(msg`Subscribe to this list`)}
) : null} accessibilityHint="">
<NativeDropdown <View style={[palInverted.view, styles.btn]}>
testID="headerDropdownBtn" <Text style={palInverted.text}>
items={dropdownItems} <Trans>Subscribe</Trans>
accessibilityLabel={_(msg`More options`)} </Text>
accessibilityHint=""> </View>
<View style={[pal.viewLight, styles.btn]}> </NativeDropdown>
<FontAwesomeIcon icon="ellipsis" size={20} color={pal.colors.text} /> )
) : null}
<NativeDropdown
testID="headerDropdownBtn"
items={dropdownItems}
accessibilityLabel={_(msg`More options`)}
accessibilityHint="">
<View style={[pal.viewLight, styles.btn]}>
<FontAwesomeIcon
icon="ellipsis"
size={20}
color={pal.colors.text}
/>
</View>
</NativeDropdown>
<Prompt.Basic
control={deleteListPromptControl}
title={_(msg`Delete this list?`)}
description={_(
msg`If you delete this list, you won't be able to recover it.`,
)}
onConfirm={onPressDelete}
confirmButtonCta={_(msg`Delete`)}
confirmButtonColor="negative"
/>
<Prompt.Basic
control={subscribeMutePromptControl}
title={_(msg`Mute these accounts?`)}
description={_(
msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
)}
onConfirm={onSubscribeMute}
confirmButtonCta={_(msg`Mute list`)}
/>
<Prompt.Basic
control={subscribeBlockPromptControl}
title={_(msg`Block these accounts?`)}
description={_(
msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
)}
onConfirm={onSubscribeBlock}
confirmButtonCta={_(msg`Block list`)}
confirmButtonColor="negative"
/>
</ProfileSubpageHeader>
{descriptionRT ? (
<View style={[a.px_lg, a.pt_sm, a.pb_sm, a.gap_md]}>
<RichText value={descriptionRT} style={[a.text_md, a.leading_snug]} />
</View> </View>
</NativeDropdown> ) : null}
</>
<Prompt.Basic
control={deleteListPromptControl}
title={_(msg`Delete this list?`)}
description={_(
msg`If you delete this list, you won't be able to recover it.`,
)}
onConfirm={onPressDelete}
confirmButtonCta={_(msg`Delete`)}
confirmButtonColor="negative"
/>
<Prompt.Basic
control={subscribeMutePromptControl}
title={_(msg`Mute these accounts?`)}
description={_(
msg`Muting is private. Muted accounts can interact with you, but you will not see their posts or receive notifications from them.`,
)}
onConfirm={onSubscribeMute}
confirmButtonCta={_(msg`Mute list`)}
/>
<Prompt.Basic
control={subscribeBlockPromptControl}
title={_(msg`Block these accounts?`)}
description={_(
msg`Blocking is public. Blocked accounts cannot reply in your threads, mention you, or otherwise interact with you.`,
)}
onConfirm={onSubscribeBlock}
confirmButtonCta={_(msg`Block list`)}
confirmButtonColor="negative"
/>
</ProfileSubpageHeader>
) )
} }
@@ -825,25 +841,12 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
ref, ref,
) { ) {
const pal = usePalette('default') const pal = usePalette('default')
const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const isCurateList = list.purpose === 'app.bsky.graph.defs#curatelist'
const isOwner = list.creator.did === currentAccount?.did const isOwner = list.creator.did === currentAccount?.did
const descriptionRT = useMemo(
() =>
list.description
? new RichTextAPI({
text: list.description,
facets: list.descriptionFacets,
})
: undefined,
[list],
)
const onScrollToTop = useCallback(() => { const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({ scrollElRef.current?.scrollToOffset({
animated: isNative, animated: isNative,
@@ -856,59 +859,11 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
})) }))
const renderHeader = React.useCallback(() => { const renderHeader = React.useCallback(() => {
if (!isOwner) {
return <View />
}
return ( return (
<View> <View style={a.pt_lg}>
<View
style={[
{
borderTopWidth: StyleSheet.hairlineWidth,
padding: isMobile ? 14 : 20,
gap: 12,
},
pal.border,
]}>
{descriptionRT ? (
<RichText
testID="listDescription"
style={[a.text_md]}
value={descriptionRT}
/>
) : (
<Text
testID="listDescriptionEmpty"
type="lg"
style={[{fontStyle: 'italic'}, pal.textLight]}>
<Trans>No description</Trans>
</Text>
)}
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
{isCurateList ? (
isOwner ? (
<Trans>User list by you</Trans>
) : (
<Trans>
User list by{' '}
<TextLink
text={sanitizeHandle(list.creator.handle || '', '@')}
href={makeProfileLink(list.creator)}
style={pal.textLight}
/>
</Trans>
)
) : isOwner ? (
<Trans>Moderation list by you</Trans>
) : (
<Trans>
Moderation list by{' '}
<TextLink
text={sanitizeHandle(list.creator.handle || '', '@')}
href={makeProfileLink(list.creator)}
style={pal.textLight}
/>
</Trans>
)}
</Text>
</View>
<View <View
style={[ style={[
{ {
@@ -919,9 +874,6 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
paddingBottom: isMobile ? 14 : 18, paddingBottom: isMobile ? 14 : 18,
}, },
]}> ]}>
<Text type="lg-bold" style={t.atoms.text}>
<Trans>Users</Trans>
</Text>
{isOwner && ( {isOwner && (
<Pressable <Pressable
testID="addUserBtn" testID="addUserBtn"
@@ -943,20 +895,7 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
</View> </View>
</View> </View>
) )
}, [ }, [isMobile, pal.colors.link, pal.link, isOwner, _, onPressAddUser])
isMobile,
pal.border,
pal.textLight,
pal.colors.link,
pal.link,
descriptionRT,
isCurateList,
isOwner,
list.creator,
t.atoms.text,
_,
onPressAddUser,
])
const renderEmptyState = useCallback(() => { const renderEmptyState = useCallback(() => {
return ( return (