Files
bsky-social-app/src/view/com/feeds/FeedSourceCard.tsx
T
Eric Bailey bd17f04810 Migrate to @bsky.app/alf package (#9030)
* Add storybook shortcut in dev

* Migrate to alg pkg

* Migrate font_bold to new font_semi_bold

* Migrate font_heavy to font_bold

* Fix old theme refs

* Remove leading util

* Bump

* Revert "Add storybook shortcut in dev"

This reverts commit 7a86195c77fb5d449c7782e64ebabb6addfab919.

* Remove alf script

* Adjust font scale

* Bump

* Bump pkg

* Migrate values from conflicts

* Create default themes outside react

* Use built-in alf utils

* Migrate old font styles

* Remove unused tokens

* Move theme creation to more appropriate file

* Update button colors

* Adjust TextField colors, line heights
2025-09-24 11:47:08 -05:00

221 lines
5.3 KiB
TypeScript

import {type StyleProp, View, type ViewStyle} from 'react-native'
import {
type $Typed,
AppBskyFeedDefs,
type AppBskyGraphDefs,
AtUri,
} from '@atproto/api'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeHandle} from '#/lib/strings/handles'
import {
type FeedSourceInfo,
hydrateFeedGenerator,
hydrateList,
useFeedSourceInfoQuery,
} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {Link} from '#/components/Link'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import {MissingFeed} from './MissingFeed'
type FeedSourceCardProps = {
feedUri: string
feedData?:
| $Typed<AppBskyFeedDefs.GeneratorView>
| $Typed<AppBskyGraphDefs.ListView>
style?: StyleProp<ViewStyle>
showSaveBtn?: boolean
showDescription?: boolean
showLikes?: boolean
pinOnSave?: boolean
showMinimalPlaceholder?: boolean
hideTopBorder?: boolean
link?: boolean
}
export function FeedSourceCard({
feedUri,
feedData,
...props
}: FeedSourceCardProps) {
if (feedData) {
let feed: FeedSourceInfo
if (AppBskyFeedDefs.isGeneratorView(feedData)) {
feed = hydrateFeedGenerator(feedData)
} else {
feed = hydrateList(feedData)
}
return <FeedSourceCardLoaded feedUri={feedUri} feed={feed} {...props} />
} else {
return <FeedSourceCardWithoutData feedUri={feedUri} {...props} />
}
}
export function FeedSourceCardWithoutData({
feedUri,
...props
}: Omit<FeedSourceCardProps, 'feedData'>) {
const {data: feed, error} = useFeedSourceInfoQuery({
uri: feedUri,
})
return (
<FeedSourceCardLoaded
feedUri={feedUri}
feed={feed}
error={error}
{...props}
/>
)
}
export function FeedSourceCardLoaded({
feedUri,
feed,
style,
showDescription = false,
showLikes = false,
showMinimalPlaceholder,
hideTopBorder,
link = true,
error,
}: {
feedUri: string
feed?: FeedSourceInfo
style?: StyleProp<ViewStyle>
showDescription?: boolean
showLikes?: boolean
showMinimalPlaceholder?: boolean
hideTopBorder?: boolean
link?: boolean
error?: unknown
}) {
const t = useTheme()
const {_} = useLingui()
/*
* LOAD STATE
*
* This state also captures the scenario where a feed can't load for whatever
* reason.
*/
if (!feed) {
if (error) {
return (
<MissingFeed
uri={feedUri}
style={style}
hideTopBorder={hideTopBorder}
error={error}
/>
)
}
return (
<FeedLoadingPlaceholder
style={[
t.atoms.border_contrast_low,
!(showMinimalPlaceholder || hideTopBorder) && a.border_t,
a.flex_1,
style,
]}
showTopBorder={false}
showLowerPlaceholder={!showMinimalPlaceholder}
/>
)
}
const inner = (
<>
<View style={[a.flex_row, a.align_center]}>
<View style={[a.mr_md]}>
<UserAvatar type="algo" size={36} avatar={feed.avatar} />
</View>
<View style={[a.flex_1]}>
<Text
emoji
style={[a.text_sm, a.font_semi_bold, a.leading_snug]}
numberOfLines={1}>
{feed.displayName}
</Text>
<Text
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_snug]}
numberOfLines={1}>
{feed.type === 'feed' ? (
<Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
) : (
<Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
)}
</Text>
</View>
</View>
{showDescription && feed.description ? (
<RichText
style={[t.atoms.text_contrast_high, a.flex_1, a.flex_wrap]}
value={feed.description}
numberOfLines={3}
/>
) : null}
{showLikes && feed.type === 'feed' ? (
<Text
style={[
a.text_sm,
a.font_semi_bold,
t.atoms.text_contrast_medium,
a.leading_snug,
]}>
<Trans>
Liked by{' '}
<Plural value={feed.likeCount || 0} one="# user" other="# users" />
</Trans>
</Text>
) : null}
</>
)
if (link) {
return (
<Link
testID={`feed-${feed.displayName}`}
label={_(
feed.type === 'feed'
? msg`${feed.displayName}, a feed by ${sanitizeHandle(feed.creatorHandle, '@')}, liked by ${feed.likeCount || 0}`
: msg`${feed.displayName}, a list by ${sanitizeHandle(feed.creatorHandle, '@')}`,
)}
to={{
screen: feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList',
params: {name: feed.creatorDid, rkey: new AtUri(feed.uri).rkey},
}}
style={[
a.flex_1,
a.p_lg,
a.gap_md,
!hideTopBorder && !a.border_t,
t.atoms.border_contrast_low,
style,
]}>
{inner}
</Link>
)
} else {
return (
<View
style={[
a.flex_1,
a.p_lg,
a.gap_md,
!hideTopBorder && !a.border_t,
t.atoms.border_contrast_low,
style,
]}>
{inner}
</View>
)
}
}