Pinned feeds cards (#4526)

* Add lists support to FeedCard

* Add useSavedFeeds query, similar to usePinnedFeedInfos

* Integrate into Feeds screen

* Fix alignment on mobile

* Update usages

* Add placeholder loading state

* Handle no feeds state

* Reuse previous data for placeholder

* Staged loading

* Improve staged loading

* Use setQueryData approach to pre-caching

* Add types for a little more safety

* Fix precaching

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
This commit is contained in:
Eric Bailey 2024-06-21 16:50:23 -05:00 committed by GitHub
parent cb37647949
commit 4d6787009c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 447 additions and 233 deletions

View file

@ -1,6 +1,11 @@
import React from 'react'
import {GestureResponderEvent, View} from 'react-native'
import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyGraphDefs,
AtUri,
} from '@atproto/api'
import {msg, plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@ -20,23 +25,35 @@ import {Button, ButtonIcon} from '#/components/Button'
import {useRichText} from '#/components/hooks/useRichText'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {Link as InternalLink} from '#/components/Link'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
export function Default({feed}: {feed: AppBskyFeedDefs.GeneratorView}) {
export function Default({
type,
view,
}:
| {
type: 'feed'
view: AppBskyFeedDefs.GeneratorView
}
| {
type: 'list'
view: AppBskyGraphDefs.ListView
}) {
const displayName = type === 'feed' ? view.displayName : view.name
return (
<Link feed={feed}>
<Link feed={view}>
<Outer>
<Header>
<Avatar src={feed.avatar} />
<TitleAndByline title={feed.displayName} creator={feed.creator} />
<Action uri={feed.uri} pin />
<Avatar src={view.avatar} />
<TitleAndByline title={displayName} creator={view.creator} />
<Action uri={view.uri} pin />
</Header>
<Description description={feed.description} />
<Likes count={feed.likeCount || 0} />
<Description description={view.description} />
{type === 'feed' && <Likes count={view.likeCount || 0} />}
</Outer>
</Link>
)
@ -46,13 +63,10 @@ export function Link({
children,
feed,
}: {
children: React.ReactElement
feed: AppBskyFeedDefs.GeneratorView
}) {
feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView
} & Omit<LinkProps, 'to'>) {
const href = React.useMemo(() => {
const urip = new AtUri(feed.uri)
const handleOrDid = feed.creator.handle || feed.creator.did
return `/profile/${handleOrDid}/feed/${urip.rkey}`
return createProfileFeedHref({feed})
}, [feed])
return <InternalLink to={href}>{children}</InternalLink>
}
@ -62,11 +76,33 @@ export function Outer({children}: {children: React.ReactNode}) {
}
export function Header({children}: {children: React.ReactNode}) {
return <View style={[a.flex_row, a.align_center, a.gap_md]}>{children}</View>
return (
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_md]}>
{children}
</View>
)
}
export function Avatar({src}: {src: string | undefined}) {
return <UserAvatar type="algo" size={40} avatar={src} />
export type AvatarProps = {src: string | undefined; size?: number}
export function Avatar({src, size = 40}: AvatarProps) {
return <UserAvatar type="algo" size={size} avatar={src} />
}
export function AvatarPlaceholder({size = 40}: Omit<AvatarProps, 'src'>) {
const t = useTheme()
return (
<View
style={[
t.atoms.bg_contrast_25,
{
width: size,
height: size,
borderRadius: 8,
},
]}
/>
)
}
export function TitleAndByline({
@ -74,22 +110,54 @@ export function TitleAndByline({
creator,
}: {
title: string
creator: AppBskyActorDefs.ProfileViewBasic
creator?: AppBskyActorDefs.ProfileViewBasic
}) {
const t = useTheme()
return (
<View style={[a.flex_1]}>
<Text
style={[a.text_md, a.font_bold, a.flex_1, a.leading_snug]}
numberOfLines={1}>
<Text style={[a.text_md, a.font_bold, a.leading_snug]} numberOfLines={1}>
{title}
</Text>
<Text
style={[a.flex_1, a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
<Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans>
</Text>
{creator && (
<Text
style={[a.leading_snug, t.atoms.text_contrast_medium]}
numberOfLines={1}>
<Trans>Feed by {sanitizeHandle(creator.handle, '@')}</Trans>
</Text>
)}
</View>
)
}
export function TitleAndBylinePlaceholder({creator}: {creator?: boolean}) {
const t = useTheme()
return (
<View style={[a.flex_1, a.gap_xs]}>
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_50,
{
width: '60%',
height: 14,
},
]}
/>
{creator && (
<View
style={[
a.rounded_xs,
t.atoms.bg_contrast_25,
{
width: '40%',
height: 10,
},
]}
/>
)}
</View>
)
}
@ -203,3 +271,16 @@ function ActionInner({uri, pin}: {uri: string; pin?: boolean}) {
</>
)
}
export function createProfileFeedHref({
feed,
}: {
feed: AppBskyFeedDefs.GeneratorView | AppBskyGraphDefs.ListView
}) {
const urip = new AtUri(feed.uri)
const type = urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'list'
const handleOrDid = feed.creator.handle || feed.creator.did
return `/profile/${handleOrDid}/${type === 'feed' ? 'feed' : 'lists'}/${
urip.rkey
}`
}