Fix quote+list card padding (#8623)

* fix quote padding not being pressable

* fix list padding not being pressable

* Fix unnecessary loading of feeds (#8578)

* stop layout shifts in feed loading

* don't load feed data if we already have it

* adjust styles, alf stuff

* remove unused button, massively simplify

* fix layout shifting in notifs

* use feedcard for feed post embeds

* use bold text to match other style

* use Link component rather than jank Pressable

* prevent nested anchors in notifs

* match following text size

* add space between content hider

* Better dead feed handling (#8579)

* add space between content hider

* add handling for feeds that fail to load

* cleanError, in case of network funkiness

* handle deleted lists

* split out missingfeed
This commit is contained in:
Samuel Newman
2025-07-09 10:14:30 +03:00
committed by GitHub
parent 1fcd313424
commit 07b028ee66
12 changed files with 499 additions and 431 deletions
+2 -9
View File
@@ -1,18 +1,11 @@
import {View} from 'react-native'
import {atoms as a, flatten, useTheme, ViewStyleProp} from '#/alf'
import {atoms as a, useTheme, type ViewStyleProp} from '#/alf'
export function Divider({style}: ViewStyleProp) {
const t = useTheme()
return (
<View
style={[
a.w_full,
a.border_t,
t.atoms.border_contrast_low,
flatten(style),
]}
/>
<View style={[a.w_full, a.border_t, t.atoms.border_contrast_low, style]} />
)
}
+1 -1
View File
@@ -214,7 +214,7 @@ export function DescriptionPlaceholder() {
export function Likes({count}: {count: number}) {
const t = useTheme()
return (
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
<Text style={[a.text_sm, t.atoms.text_contrast_medium, a.font_bold]}>
<Trans>
Liked by <Plural value={count || 0} one="# user" other="# users" />
</Trans>
+7 -5
View File
@@ -1,10 +1,10 @@
import React from 'react'
import {View} from 'react-native'
import {
AppBskyGraphDefs,
type AppBskyGraphDefs,
AtUri,
moderateUserList,
ModerationUI,
type ModerationUI,
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -22,10 +22,10 @@ import {
Outer,
SaveButton,
} from '#/components/FeedCard'
import {Link as InternalLink, LinkProps} from '#/components/Link'
import {Link as InternalLink, type LinkProps} from '#/components/Link'
import * as Hider from '#/components/moderation/Hider'
import {Text} from '#/components/Typography'
import * as bsky from '#/types/bsky'
import type * as bsky from '#/types/bsky'
/*
* This component is based on `FeedCard` and is tightly coupled with that
@@ -50,7 +50,9 @@ type Props = {
showPinButton?: boolean
}
export function Default(props: Props) {
export function Default(
props: Props & Omit<LinkProps, 'to' | 'label' | 'children'>,
) {
const {view, showPinButton} = props
const moderationOpts = useModerationOpts()
const moderation = moderationOpts
+22 -21
View File
@@ -1,10 +1,9 @@
import React from 'react'
import {StyleSheet} from 'react-native'
import {useMemo} from 'react'
import {moderateFeedGenerator} from '@atproto/api'
import {usePalette} from '#/lib/hooks/usePalette'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard'
import {atoms as a, useTheme} from '#/alf'
import * as FeedCard from '#/components/FeedCard'
import {ContentHider} from '#/components/moderation/ContentHider'
import {type EmbedType} from '#/types/bsky/post'
import {type CommonProps} from './types'
@@ -14,13 +13,22 @@ export function FeedEmbed({
}: CommonProps & {
embed: EmbedType<'feed'>
}) {
const pal = usePalette('default')
const t = useTheme()
return (
<FeedSourceCard
feedUri={embed.view.uri}
style={[pal.view, pal.border, styles.customFeedOuter]}
showLikes
/>
<FeedCard.Link
view={embed.view}
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}>
<FeedCard.Outer>
<FeedCard.Header>
<FeedCard.Avatar src={embed.view.avatar} />
<FeedCard.TitleAndByline
title={embed.view.displayName}
creator={embed.view.creator}
/>
</FeedCard.Header>
<FeedCard.Likes count={embed.view.likeCount || 0} />
</FeedCard.Outer>
</FeedCard.Link>
)
}
@@ -30,23 +38,16 @@ export function ModeratedFeedEmbed({
embed: EmbedType<'feed'>
}) {
const moderationOpts = useModerationOpts()
const moderation = React.useMemo(() => {
const moderation = useMemo(() => {
return moderationOpts
? moderateFeedGenerator(embed.view, moderationOpts)
: undefined
}, [embed.view, moderationOpts])
return (
<ContentHider modui={moderation?.ui('contentList')}>
<ContentHider
modui={moderation?.ui('contentList')}
childContainerStyle={[a.pt_xs]}>
<FeedEmbed embed={embed} />
</ContentHider>
)
}
const styles = StyleSheet.create({
customFeedOuter: {
borderWidth: StyleSheet.hairlineWidth,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12,
},
})
+9 -8
View File
@@ -1,5 +1,4 @@
import React from 'react'
import {View} from 'react-native'
import {useMemo} from 'react'
import {moderateUserList} from '@atproto/api'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
@@ -16,10 +15,10 @@ export function ListEmbed({
}) {
const t = useTheme()
return (
<View
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}>
<ListCard.Default view={embed.view} />
</View>
<ListCard.Default
view={embed.view}
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}
/>
)
}
@@ -29,13 +28,15 @@ export function ModeratedListEmbed({
embed: EmbedType<'list'>
}) {
const moderationOpts = useModerationOpts()
const moderation = React.useMemo(() => {
const moderation = useMemo(() => {
return moderationOpts
? moderateUserList(embed.view, moderationOpts)
: undefined
}, [embed.view, moderationOpts])
return (
<ContentHider modui={moderation?.ui('contentList')}>
<ContentHider
modui={moderation?.ui('contentList')}
childContainerStyle={[a.pt_xs]}>
<ListEmbed embed={embed} />
</ContentHider>
)
+51 -55
View File
@@ -268,64 +268,60 @@ export function QuoteEmbed({
const [hover, setHover] = React.useState(false)
return (
<View
onPointerEnter={() => {
setHover(true)
}}
onPointerLeave={() => {
setHover(false)
}}>
style={[a.mt_sm]}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}>
<ContentHider
modui={moderation?.ui('contentList')}
style={[
a.rounded_md,
a.p_md,
a.mt_sm,
a.border,
t.atoms.border_contrast_low,
style,
]}
style={[a.rounded_md, a.border, t.atoms.border_contrast_low, style]}
activeStyle={[a.p_md, a.pt_sm]}
childContainerStyle={[a.pt_sm]}>
<SubtleWebHover hover={hover} />
<Link
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
href={itemHref}
title={itemTitle}
onBeforePress={onBeforePress}>
<View pointerEvents="none">
<PostMeta
author={quote.author}
moderation={moderation}
showAvatar
postHref={itemHref}
timestamp={quote.indexedAt}
/>
</View>
{moderation ? (
<PostAlerts
modui={moderation.ui('contentView')}
style={[a.py_xs]}
/>
) : null}
{richText ? (
<RichText
value={richText}
style={a.text_md}
numberOfLines={20}
disableLinks
/>
) : null}
{quote.embed && (
<Embed
embed={quote.embed}
moderation={moderation}
isWithinQuote={parentIsWithinQuote ?? true}
// already within quote? override nested
allowNestedQuotes={
parentIsWithinQuote ? false : parentAllowNestedQuotes
}
/>
)}
</Link>
{({active}) => (
<>
{!active && <SubtleWebHover hover={hover} style={[a.rounded_md]} />}
<Link
style={[!active && a.p_md]}
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
href={itemHref}
title={itemTitle}
onBeforePress={onBeforePress}>
<View pointerEvents="none">
<PostMeta
author={quote.author}
moderation={moderation}
showAvatar
postHref={itemHref}
timestamp={quote.indexedAt}
/>
</View>
{moderation ? (
<PostAlerts
modui={moderation.ui('contentView')}
style={[a.py_xs]}
/>
) : null}
{richText ? (
<RichText
value={richText}
style={a.text_md}
numberOfLines={20}
disableLinks
/>
) : null}
{quote.embed && (
<Embed
embed={quote.embed}
moderation={moderation}
isWithinQuote={parentIsWithinQuote ?? true}
// already within quote? override nested
allowNestedQuotes={
parentIsWithinQuote ? false : parentAllowNestedQuotes
}
/>
)}
</Link>
</>
)}
</ContentHider>
</View>
)
+8 -5
View File
@@ -23,20 +23,23 @@ export function ContentHider({
modui,
ignoreMute,
style,
activeStyle,
childContainerStyle,
children,
}: React.PropsWithChildren<{
}: {
testID?: string
modui: ModerationUI | undefined
ignoreMute?: boolean
style?: StyleProp<ViewStyle>
activeStyle?: StyleProp<ViewStyle>
childContainerStyle?: StyleProp<ViewStyle>
}>) {
children?: React.ReactNode | ((props: {active: boolean}) => React.ReactNode)
}) {
const blur = modui?.blurs[0]
if (!blur || (ignoreMute && isJustAMute(modui))) {
return (
<View testID={testID} style={style}>
{children}
{typeof children === 'function' ? children({active: false}) : children}
</View>
)
}
@@ -44,9 +47,9 @@ export function ContentHider({
<ContentHiderActive
testID={testID}
modui={modui}
style={style}
style={[style, activeStyle]}
childContainerStyle={childContainerStyle}>
{children}
{typeof children === 'function' ? children({active: true}) : children}
</ContentHiderActive>
)
}