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:
@@ -1,18 +1,11 @@
|
|||||||
import {View} from 'react-native'
|
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) {
|
export function Divider({style}: ViewStyleProp) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={[a.w_full, a.border_t, t.atoms.border_contrast_low, style]} />
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
a.border_t,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
flatten(style),
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export function DescriptionPlaceholder() {
|
|||||||
export function Likes({count}: {count: number}) {
|
export function Likes({count}: {count: number}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
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>
|
<Trans>
|
||||||
Liked by <Plural value={count || 0} one="# user" other="# users" />
|
Liked by <Plural value={count || 0} one="# user" other="# users" />
|
||||||
</Trans>
|
</Trans>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyGraphDefs,
|
type AppBskyGraphDefs,
|
||||||
AtUri,
|
AtUri,
|
||||||
moderateUserList,
|
moderateUserList,
|
||||||
ModerationUI,
|
type ModerationUI,
|
||||||
} from '@atproto/api'
|
} 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'
|
||||||
@@ -22,10 +22,10 @@ import {
|
|||||||
Outer,
|
Outer,
|
||||||
SaveButton,
|
SaveButton,
|
||||||
} from '#/components/FeedCard'
|
} 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 * as Hider from '#/components/moderation/Hider'
|
||||||
import {Text} from '#/components/Typography'
|
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
|
* This component is based on `FeedCard` and is tightly coupled with that
|
||||||
@@ -50,7 +50,9 @@ type Props = {
|
|||||||
showPinButton?: boolean
|
showPinButton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Default(props: Props) {
|
export function Default(
|
||||||
|
props: Props & Omit<LinkProps, 'to' | 'label' | 'children'>,
|
||||||
|
) {
|
||||||
const {view, showPinButton} = props
|
const {view, showPinButton} = props
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const moderation = moderationOpts
|
const moderation = moderationOpts
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from 'react'
|
import {useMemo} from 'react'
|
||||||
import {StyleSheet} from 'react-native'
|
|
||||||
import {moderateFeedGenerator} from '@atproto/api'
|
import {moderateFeedGenerator} from '@atproto/api'
|
||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
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 {ContentHider} from '#/components/moderation/ContentHider'
|
||||||
import {type EmbedType} from '#/types/bsky/post'
|
import {type EmbedType} from '#/types/bsky/post'
|
||||||
import {type CommonProps} from './types'
|
import {type CommonProps} from './types'
|
||||||
@@ -14,13 +13,22 @@ export function FeedEmbed({
|
|||||||
}: CommonProps & {
|
}: CommonProps & {
|
||||||
embed: EmbedType<'feed'>
|
embed: EmbedType<'feed'>
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<FeedSourceCard
|
<FeedCard.Link
|
||||||
feedUri={embed.view.uri}
|
view={embed.view}
|
||||||
style={[pal.view, pal.border, styles.customFeedOuter]}
|
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}>
|
||||||
showLikes
|
<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'>
|
embed: EmbedType<'feed'>
|
||||||
}) {
|
}) {
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const moderation = React.useMemo(() => {
|
const moderation = useMemo(() => {
|
||||||
return moderationOpts
|
return moderationOpts
|
||||||
? moderateFeedGenerator(embed.view, moderationOpts)
|
? moderateFeedGenerator(embed.view, moderationOpts)
|
||||||
: undefined
|
: undefined
|
||||||
}, [embed.view, moderationOpts])
|
}, [embed.view, moderationOpts])
|
||||||
return (
|
return (
|
||||||
<ContentHider modui={moderation?.ui('contentList')}>
|
<ContentHider
|
||||||
|
modui={moderation?.ui('contentList')}
|
||||||
|
childContainerStyle={[a.pt_xs]}>
|
||||||
<FeedEmbed embed={embed} />
|
<FeedEmbed embed={embed} />
|
||||||
</ContentHider>
|
</ContentHider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
customFeedOuter: {
|
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
paddingVertical: 12,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React from 'react'
|
import {useMemo} from 'react'
|
||||||
import {View} from 'react-native'
|
|
||||||
import {moderateUserList} from '@atproto/api'
|
import {moderateUserList} from '@atproto/api'
|
||||||
|
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
@@ -16,10 +15,10 @@ export function ListEmbed({
|
|||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<View
|
<ListCard.Default
|
||||||
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}>
|
view={embed.view}
|
||||||
<ListCard.Default view={embed.view} />
|
style={[a.border, t.atoms.border_contrast_medium, a.p_md, a.rounded_sm]}
|
||||||
</View>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,13 +28,15 @@ export function ModeratedListEmbed({
|
|||||||
embed: EmbedType<'list'>
|
embed: EmbedType<'list'>
|
||||||
}) {
|
}) {
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const moderation = React.useMemo(() => {
|
const moderation = useMemo(() => {
|
||||||
return moderationOpts
|
return moderationOpts
|
||||||
? moderateUserList(embed.view, moderationOpts)
|
? moderateUserList(embed.view, moderationOpts)
|
||||||
: undefined
|
: undefined
|
||||||
}, [embed.view, moderationOpts])
|
}, [embed.view, moderationOpts])
|
||||||
return (
|
return (
|
||||||
<ContentHider modui={moderation?.ui('contentList')}>
|
<ContentHider
|
||||||
|
modui={moderation?.ui('contentList')}
|
||||||
|
childContainerStyle={[a.pt_xs]}>
|
||||||
<ListEmbed embed={embed} />
|
<ListEmbed embed={embed} />
|
||||||
</ContentHider>
|
</ContentHider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -268,64 +268,60 @@ export function QuoteEmbed({
|
|||||||
const [hover, setHover] = React.useState(false)
|
const [hover, setHover] = React.useState(false)
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
onPointerEnter={() => {
|
style={[a.mt_sm]}
|
||||||
setHover(true)
|
onPointerEnter={() => setHover(true)}
|
||||||
}}
|
onPointerLeave={() => setHover(false)}>
|
||||||
onPointerLeave={() => {
|
|
||||||
setHover(false)
|
|
||||||
}}>
|
|
||||||
<ContentHider
|
<ContentHider
|
||||||
modui={moderation?.ui('contentList')}
|
modui={moderation?.ui('contentList')}
|
||||||
style={[
|
style={[a.rounded_md, a.border, t.atoms.border_contrast_low, style]}
|
||||||
a.rounded_md,
|
activeStyle={[a.p_md, a.pt_sm]}
|
||||||
a.p_md,
|
|
||||||
a.mt_sm,
|
|
||||||
a.border,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
style,
|
|
||||||
]}
|
|
||||||
childContainerStyle={[a.pt_sm]}>
|
childContainerStyle={[a.pt_sm]}>
|
||||||
<SubtleWebHover hover={hover} />
|
{({active}) => (
|
||||||
<Link
|
<>
|
||||||
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
{!active && <SubtleWebHover hover={hover} style={[a.rounded_md]} />}
|
||||||
href={itemHref}
|
<Link
|
||||||
title={itemTitle}
|
style={[!active && a.p_md]}
|
||||||
onBeforePress={onBeforePress}>
|
hoverStyle={{borderColor: pal.colors.borderLinkHover}}
|
||||||
<View pointerEvents="none">
|
href={itemHref}
|
||||||
<PostMeta
|
title={itemTitle}
|
||||||
author={quote.author}
|
onBeforePress={onBeforePress}>
|
||||||
moderation={moderation}
|
<View pointerEvents="none">
|
||||||
showAvatar
|
<PostMeta
|
||||||
postHref={itemHref}
|
author={quote.author}
|
||||||
timestamp={quote.indexedAt}
|
moderation={moderation}
|
||||||
/>
|
showAvatar
|
||||||
</View>
|
postHref={itemHref}
|
||||||
{moderation ? (
|
timestamp={quote.indexedAt}
|
||||||
<PostAlerts
|
/>
|
||||||
modui={moderation.ui('contentView')}
|
</View>
|
||||||
style={[a.py_xs]}
|
{moderation ? (
|
||||||
/>
|
<PostAlerts
|
||||||
) : null}
|
modui={moderation.ui('contentView')}
|
||||||
{richText ? (
|
style={[a.py_xs]}
|
||||||
<RichText
|
/>
|
||||||
value={richText}
|
) : null}
|
||||||
style={a.text_md}
|
{richText ? (
|
||||||
numberOfLines={20}
|
<RichText
|
||||||
disableLinks
|
value={richText}
|
||||||
/>
|
style={a.text_md}
|
||||||
) : null}
|
numberOfLines={20}
|
||||||
{quote.embed && (
|
disableLinks
|
||||||
<Embed
|
/>
|
||||||
embed={quote.embed}
|
) : null}
|
||||||
moderation={moderation}
|
{quote.embed && (
|
||||||
isWithinQuote={parentIsWithinQuote ?? true}
|
<Embed
|
||||||
// already within quote? override nested
|
embed={quote.embed}
|
||||||
allowNestedQuotes={
|
moderation={moderation}
|
||||||
parentIsWithinQuote ? false : parentAllowNestedQuotes
|
isWithinQuote={parentIsWithinQuote ?? true}
|
||||||
}
|
// already within quote? override nested
|
||||||
/>
|
allowNestedQuotes={
|
||||||
)}
|
parentIsWithinQuote ? false : parentAllowNestedQuotes
|
||||||
</Link>
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ContentHider>
|
</ContentHider>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,20 +23,23 @@ export function ContentHider({
|
|||||||
modui,
|
modui,
|
||||||
ignoreMute,
|
ignoreMute,
|
||||||
style,
|
style,
|
||||||
|
activeStyle,
|
||||||
childContainerStyle,
|
childContainerStyle,
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<{
|
}: {
|
||||||
testID?: string
|
testID?: string
|
||||||
modui: ModerationUI | undefined
|
modui: ModerationUI | undefined
|
||||||
ignoreMute?: boolean
|
ignoreMute?: boolean
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
|
activeStyle?: StyleProp<ViewStyle>
|
||||||
childContainerStyle?: StyleProp<ViewStyle>
|
childContainerStyle?: StyleProp<ViewStyle>
|
||||||
}>) {
|
children?: React.ReactNode | ((props: {active: boolean}) => React.ReactNode)
|
||||||
|
}) {
|
||||||
const blur = modui?.blurs[0]
|
const blur = modui?.blurs[0]
|
||||||
if (!blur || (ignoreMute && isJustAMute(modui))) {
|
if (!blur || (ignoreMute && isJustAMute(modui))) {
|
||||||
return (
|
return (
|
||||||
<View testID={testID} style={style}>
|
<View testID={testID} style={style}>
|
||||||
{children}
|
{typeof children === 'function' ? children({active: false}) : children}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -44,9 +47,9 @@ export function ContentHider({
|
|||||||
<ContentHiderActive
|
<ContentHiderActive
|
||||||
testID={testID}
|
testID={testID}
|
||||||
modui={modui}
|
modui={modui}
|
||||||
style={style}
|
style={[style, activeStyle]}
|
||||||
childContainerStyle={childContainerStyle}>
|
childContainerStyle={childContainerStyle}>
|
||||||
{children}
|
{typeof children === 'function' ? children({active: true}) : children}
|
||||||
</ContentHiderActive>
|
</ContentHiderActive>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,33 @@
|
|||||||
import React from 'react'
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {
|
import {
|
||||||
Linking,
|
type $Typed,
|
||||||
Pressable,
|
AppBskyFeedDefs,
|
||||||
StyleProp,
|
type AppBskyGraphDefs,
|
||||||
StyleSheet,
|
AtUri,
|
||||||
View,
|
} from '@atproto/api'
|
||||||
ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import {AtUri} from '@atproto/api'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {msg, Plural, Trans} from '@lingui/macro'
|
import {msg, Plural, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {s} from '#/lib/styles'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
|
|
||||||
import {
|
import {
|
||||||
useAddSavedFeedsMutation,
|
type FeedSourceInfo,
|
||||||
usePreferencesQuery,
|
hydrateFeedGenerator,
|
||||||
UsePreferencesQueryResponse,
|
hydrateList,
|
||||||
useRemoveFeedMutation,
|
useFeedSourceInfoQuery,
|
||||||
} from '#/state/queries/preferences'
|
} from '#/state/queries/feed'
|
||||||
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {atoms as a} from '#/alf'
|
import {Link} from '#/components/Link'
|
||||||
import {shouldClickOpenNewTab} from '#/components/Link'
|
|
||||||
import * as Prompt from '#/components/Prompt'
|
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {Text} from '../util/text/Text'
|
import {Text} from '#/components/Typography'
|
||||||
import {UserAvatar} from '../util/UserAvatar'
|
import {MissingFeed} from './MissingFeed'
|
||||||
|
|
||||||
export function FeedSourceCard({
|
type FeedSourceCardProps = {
|
||||||
feedUri,
|
|
||||||
style,
|
|
||||||
showSaveBtn = false,
|
|
||||||
showDescription = false,
|
|
||||||
showLikes = false,
|
|
||||||
pinOnSave = false,
|
|
||||||
showMinimalPlaceholder,
|
|
||||||
hideTopBorder,
|
|
||||||
}: {
|
|
||||||
feedUri: string
|
feedUri: string
|
||||||
|
feedData?:
|
||||||
|
| $Typed<AppBskyFeedDefs.GeneratorView>
|
||||||
|
| $Typed<AppBskyGraphDefs.ListView>
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
showSaveBtn?: boolean
|
showSaveBtn?: boolean
|
||||||
showDescription?: boolean
|
showDescription?: boolean
|
||||||
@@ -52,22 +35,41 @@ export function FeedSourceCard({
|
|||||||
pinOnSave?: boolean
|
pinOnSave?: boolean
|
||||||
showMinimalPlaceholder?: boolean
|
showMinimalPlaceholder?: boolean
|
||||||
hideTopBorder?: boolean
|
hideTopBorder?: boolean
|
||||||
}) {
|
link?: boolean
|
||||||
const {data: preferences} = usePreferencesQuery()
|
}
|
||||||
const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
|
|
||||||
|
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 (
|
return (
|
||||||
<FeedSourceCardLoaded
|
<FeedSourceCardLoaded
|
||||||
feedUri={feedUri}
|
feedUri={feedUri}
|
||||||
feed={feed}
|
feed={feed}
|
||||||
preferences={preferences}
|
error={error}
|
||||||
style={style}
|
{...props}
|
||||||
showSaveBtn={showSaveBtn}
|
|
||||||
showDescription={showDescription}
|
|
||||||
showLikes={showLikes}
|
|
||||||
pinOnSave={pinOnSave}
|
|
||||||
showMinimalPlaceholder={showMinimalPlaceholder}
|
|
||||||
hideTopBorder={hideTopBorder}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -75,80 +77,26 @@ export function FeedSourceCard({
|
|||||||
export function FeedSourceCardLoaded({
|
export function FeedSourceCardLoaded({
|
||||||
feedUri,
|
feedUri,
|
||||||
feed,
|
feed,
|
||||||
preferences,
|
|
||||||
style,
|
style,
|
||||||
showSaveBtn = false,
|
|
||||||
showDescription = false,
|
showDescription = false,
|
||||||
showLikes = false,
|
showLikes = false,
|
||||||
pinOnSave = false,
|
|
||||||
showMinimalPlaceholder,
|
showMinimalPlaceholder,
|
||||||
hideTopBorder,
|
hideTopBorder,
|
||||||
|
link = true,
|
||||||
|
error,
|
||||||
}: {
|
}: {
|
||||||
feedUri: string
|
feedUri: string
|
||||||
feed?: FeedSourceInfo
|
feed?: FeedSourceInfo
|
||||||
preferences?: UsePreferencesQueryResponse
|
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
showSaveBtn?: boolean
|
|
||||||
showDescription?: boolean
|
showDescription?: boolean
|
||||||
showLikes?: boolean
|
showLikes?: boolean
|
||||||
pinOnSave?: boolean
|
|
||||||
showMinimalPlaceholder?: boolean
|
showMinimalPlaceholder?: boolean
|
||||||
hideTopBorder?: boolean
|
hideTopBorder?: boolean
|
||||||
|
link?: boolean
|
||||||
|
error?: unknown
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const removePromptControl = Prompt.usePromptControl()
|
|
||||||
const navigation = useNavigationDeduped()
|
|
||||||
|
|
||||||
const {isPending: isAddSavedFeedPending, mutateAsync: addSavedFeeds} =
|
|
||||||
useAddSavedFeedsMutation()
|
|
||||||
const {isPending: isRemovePending, mutateAsync: removeFeed} =
|
|
||||||
useRemoveFeedMutation()
|
|
||||||
|
|
||||||
const savedFeedConfig = preferences?.savedFeeds?.find(
|
|
||||||
f => f.value === feedUri,
|
|
||||||
)
|
|
||||||
const isSaved = Boolean(savedFeedConfig)
|
|
||||||
|
|
||||||
const onSave = React.useCallback(async () => {
|
|
||||||
if (!feed || isSaved) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await addSavedFeeds([
|
|
||||||
{
|
|
||||||
type: 'feed',
|
|
||||||
value: feed.uri,
|
|
||||||
pinned: pinOnSave,
|
|
||||||
},
|
|
||||||
])
|
|
||||||
Toast.show(_(msg`Added to my feeds`))
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
|
|
||||||
logger.error('Failed to save feed', {message: e})
|
|
||||||
}
|
|
||||||
}, [_, feed, pinOnSave, addSavedFeeds, isSaved])
|
|
||||||
|
|
||||||
const onUnsave = React.useCallback(async () => {
|
|
||||||
if (!savedFeedConfig) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await removeFeed(savedFeedConfig)
|
|
||||||
// await item.unsave()
|
|
||||||
Toast.show(_(msg`Removed from my feeds`))
|
|
||||||
} catch (e) {
|
|
||||||
Toast.show(_(msg`There was an issue contacting your server`), 'xmark')
|
|
||||||
logger.error('Failed to unsave feed', {message: e})
|
|
||||||
}
|
|
||||||
}, [_, removeFeed, savedFeedConfig])
|
|
||||||
|
|
||||||
const onToggleSaved = React.useCallback(async () => {
|
|
||||||
if (isSaved) {
|
|
||||||
removePromptControl.open()
|
|
||||||
} else {
|
|
||||||
await onSave()
|
|
||||||
}
|
|
||||||
}, [isSaved, removePromptControl, onSave])
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* LOAD STATE
|
* LOAD STATE
|
||||||
@@ -156,200 +104,117 @@ export function FeedSourceCardLoaded({
|
|||||||
* This state also captures the scenario where a feed can't load for whatever
|
* This state also captures the scenario where a feed can't load for whatever
|
||||||
* reason.
|
* reason.
|
||||||
*/
|
*/
|
||||||
if (!feed || !preferences)
|
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_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_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 (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
pal.border,
|
a.flex_1,
|
||||||
{
|
a.p_lg,
|
||||||
borderTopWidth:
|
a.gap_md,
|
||||||
showMinimalPlaceholder || hideTopBorder
|
!hideTopBorder && !a.border_t,
|
||||||
? 0
|
t.atoms.border_contrast_low,
|
||||||
: StyleSheet.hairlineWidth,
|
style,
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1,
|
|
||||||
paddingRight: 18,
|
|
||||||
},
|
|
||||||
]}>
|
]}>
|
||||||
{showMinimalPlaceholder ? (
|
{inner}
|
||||||
<FeedLoadingPlaceholder
|
|
||||||
style={{flex: 1}}
|
|
||||||
showTopBorder={false}
|
|
||||||
showLowerPlaceholder={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FeedLoadingPlaceholder style={{flex: 1}} showTopBorder={false} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showSaveBtn && (
|
|
||||||
<Pressable
|
|
||||||
testID={`feed-${feedUri}-toggleSave`}
|
|
||||||
disabled={isRemovePending}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Remove from my feeds`)}
|
|
||||||
accessibilityHint=""
|
|
||||||
onPress={onUnsave}
|
|
||||||
hitSlop={15}
|
|
||||||
style={styles.btn}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={['far', 'trash-can']}
|
|
||||||
size={19}
|
|
||||||
color={pal.colors.icon}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Pressable
|
|
||||||
testID={`feed-${feed.displayName}`}
|
|
||||||
accessibilityRole="button"
|
|
||||||
style={[
|
|
||||||
styles.container,
|
|
||||||
pal.border,
|
|
||||||
style,
|
|
||||||
{borderTopWidth: hideTopBorder ? 0 : StyleSheet.hairlineWidth},
|
|
||||||
]}
|
|
||||||
onPress={e => {
|
|
||||||
const shouldOpenInNewTab = shouldClickOpenNewTab(e)
|
|
||||||
if (feed.type === 'feed') {
|
|
||||||
if (shouldOpenInNewTab) {
|
|
||||||
Linking.openURL(
|
|
||||||
`/profile/${feed.creatorDid}/feed/${new AtUri(feed.uri).rkey}`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
navigation.push('ProfileFeed', {
|
|
||||||
name: feed.creatorDid,
|
|
||||||
rkey: new AtUri(feed.uri).rkey,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (feed.type === 'list') {
|
|
||||||
if (shouldOpenInNewTab) {
|
|
||||||
Linking.openURL(
|
|
||||||
`/profile/${feed.creatorDid}/lists/${new AtUri(feed.uri).rkey}`,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
navigation.push('ProfileList', {
|
|
||||||
name: feed.creatorDid,
|
|
||||||
rkey: new AtUri(feed.uri).rkey,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
key={feed.uri}>
|
|
||||||
<View style={[styles.headerContainer, a.align_center]}>
|
|
||||||
<View style={[s.mr10]}>
|
|
||||||
<UserAvatar type="algo" size={36} avatar={feed.avatar} />
|
|
||||||
</View>
|
|
||||||
<View style={[styles.headerTextContainer]}>
|
|
||||||
<Text emoji style={[pal.text, s.bold]} numberOfLines={1}>
|
|
||||||
{feed.displayName}
|
|
||||||
</Text>
|
|
||||||
<Text style={[pal.textLight]} numberOfLines={1}>
|
|
||||||
{feed.type === 'feed' ? (
|
|
||||||
<Trans>Feed by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>List by {sanitizeHandle(feed.creatorHandle, '@')}</Trans>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{showSaveBtn && (
|
|
||||||
<View style={{alignSelf: 'center'}}>
|
|
||||||
<Pressable
|
|
||||||
testID={`feed-${feed.displayName}-toggleSave`}
|
|
||||||
disabled={isAddSavedFeedPending || isRemovePending}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={
|
|
||||||
isSaved
|
|
||||||
? _(msg`Remove from my feeds`)
|
|
||||||
: _(msg`Add to my feeds`)
|
|
||||||
}
|
|
||||||
accessibilityHint=""
|
|
||||||
onPress={onToggleSaved}
|
|
||||||
hitSlop={15}
|
|
||||||
style={styles.btn}>
|
|
||||||
{isSaved ? (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon={['far', 'trash-can']}
|
|
||||||
size={19}
|
|
||||||
color={pal.colors.icon}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="plus"
|
|
||||||
size={18}
|
|
||||||
color={pal.colors.link}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{showDescription && feed.description ? (
|
|
||||||
<RichText
|
|
||||||
style={[t.atoms.text_contrast_high, styles.description]}
|
|
||||||
value={feed.description}
|
|
||||||
numberOfLines={3}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{showLikes && feed.type === 'feed' ? (
|
|
||||||
<Text type="sm-medium" style={[pal.text, pal.textLight]}>
|
|
||||||
<Trans>
|
|
||||||
Liked by{' '}
|
|
||||||
<Plural
|
|
||||||
value={feed.likeCount || 0}
|
|
||||||
one="# user"
|
|
||||||
other="# users"
|
|
||||||
/>
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
<Prompt.Basic
|
|
||||||
control={removePromptControl}
|
|
||||||
title={_(msg`Remove from your feeds?`)}
|
|
||||||
description={_(
|
|
||||||
msg`Are you sure you want to remove ${feed.displayName} from your feeds?`,
|
|
||||||
)}
|
|
||||||
onConfirm={onUnsave}
|
|
||||||
confirmButtonCta={_(msg`Remove`)}
|
|
||||||
confirmButtonColor="negative"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
paddingVertical: 20,
|
|
||||||
flexDirection: 'column',
|
|
||||||
flex: 1,
|
|
||||||
gap: 14,
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
|
||||||
},
|
|
||||||
headerContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
headerTextContainer: {
|
|
||||||
flexDirection: 'column',
|
|
||||||
columnGap: 4,
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
flex: 1,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
},
|
|
||||||
btn: {
|
|
||||||
paddingVertical: 6,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import {type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
|
import {AtUri} from '@atproto/api'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
|
import {getFeedTypeFromUri} from '#/state/queries/feed'
|
||||||
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {Divider} from '#/components/Divider'
|
||||||
|
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
|
||||||
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function MissingFeed({
|
||||||
|
style,
|
||||||
|
hideTopBorder,
|
||||||
|
uri,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
|
hideTopBorder?: boolean
|
||||||
|
uri: string
|
||||||
|
error?: unknown
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const control = Dialog.useDialogControl()
|
||||||
|
|
||||||
|
const type = getFeedTypeFromUri(uri)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
label={
|
||||||
|
type === 'feed'
|
||||||
|
? _(msg`Could not connect to custom feed`)
|
||||||
|
: _(msg`Deleted list`)
|
||||||
|
}
|
||||||
|
accessibilityHint={_(msg`Tap for more information`)}
|
||||||
|
onPress={control.open}
|
||||||
|
style={[
|
||||||
|
a.flex_1,
|
||||||
|
a.p_lg,
|
||||||
|
a.gap_md,
|
||||||
|
!hideTopBorder && !a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.justify_start,
|
||||||
|
style,
|
||||||
|
]}>
|
||||||
|
<View style={[a.flex_row, a.align_center]}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
{width: 36, height: 36},
|
||||||
|
t.atoms.bg_contrast_25,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.mr_md,
|
||||||
|
a.align_center,
|
||||||
|
a.justify_center,
|
||||||
|
]}>
|
||||||
|
<WarningIcon size="lg" />
|
||||||
|
</View>
|
||||||
|
<View style={[a.flex_1]}>
|
||||||
|
<Text
|
||||||
|
emoji
|
||||||
|
style={[a.text_sm, a.font_bold, a.leading_snug, a.italic]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{type === 'feed' ? (
|
||||||
|
<Trans>Feed unavailable</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Deleted list</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_sm,
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
a.leading_snug,
|
||||||
|
a.italic,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{isWeb ? (
|
||||||
|
<Trans>Click for information</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Tap for information</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<DialogInner uri={uri} type={type} error={error} />
|
||||||
|
</Dialog.Outer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogInner({
|
||||||
|
uri,
|
||||||
|
type,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
uri: string
|
||||||
|
type: 'feed' | 'list'
|
||||||
|
error: unknown
|
||||||
|
}) {
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const atUri = new AtUri(uri)
|
||||||
|
const {data: profile, isError: isProfileError} = useProfileQuery({
|
||||||
|
did: atUri.host,
|
||||||
|
})
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
label={
|
||||||
|
type === 'feed'
|
||||||
|
? _(msg`Unavailable feed information`)
|
||||||
|
: _(msg`Deleted list`)
|
||||||
|
}
|
||||||
|
style={web({maxWidth: 500})}>
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.font_heavy, a.text_2xl]}>
|
||||||
|
{type === 'feed' ? (
|
||||||
|
<Trans>Could not connect to feed service</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>Deleted list</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text style={[t.atoms.text_contrast_high, a.leading_snug]}>
|
||||||
|
{type === 'feed' ? (
|
||||||
|
<Trans>
|
||||||
|
We could not connect to the service that provides this custom
|
||||||
|
feed. It may be temporarily unavailable and experiencing issues,
|
||||||
|
or permanently unavailable.
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>We could not find this list. It was probably deleted.</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Divider style={[a.my_md]} />
|
||||||
|
<Text style={[a.font_bold, t.atoms.text_contrast_high]}>
|
||||||
|
{type === 'feed' ? (
|
||||||
|
<Trans>Feed creator</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>List creator</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
{profile && moderationOpts && (
|
||||||
|
<View style={[a.w_full, a.align_start]}>
|
||||||
|
<ProfileCard.Link profile={profile} onPress={() => control.close()}>
|
||||||
|
<ProfileCard.Header>
|
||||||
|
<ProfileCard.Avatar
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
disabledPreview
|
||||||
|
/>
|
||||||
|
<ProfileCard.NameAndHandle
|
||||||
|
profile={profile}
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
/>
|
||||||
|
</ProfileCard.Header>
|
||||||
|
</ProfileCard.Link>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{isProfileError && (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
t.atoms.text_contrast_high,
|
||||||
|
a.italic,
|
||||||
|
a.text_center,
|
||||||
|
a.w_full,
|
||||||
|
]}>
|
||||||
|
<Trans>Could not find profile</Trans>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{type === 'feed' && (
|
||||||
|
<>
|
||||||
|
<Text style={[a.font_bold, t.atoms.text_contrast_high, a.mt_md]}>
|
||||||
|
<Trans>Feed identifier</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_md, t.atoms.text_contrast_high, a.italic]}>
|
||||||
|
{atUri.rkey}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{error instanceof Error && (
|
||||||
|
<>
|
||||||
|
<Text style={[a.font_bold, t.atoms.text_contrast_high, a.mt_md]}>
|
||||||
|
<Trans>Error message</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_md, t.atoms.text_contrast_high, a.italic]}>
|
||||||
|
{cleanError(error.message)}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
{isNative && (
|
||||||
|
<Button
|
||||||
|
label={_(msg`Close`)}
|
||||||
|
onPress={() => control.close()}
|
||||||
|
size="small"
|
||||||
|
variant="solid"
|
||||||
|
color="secondary"
|
||||||
|
style={[a.mt_5xl]}>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Dialog.Close />
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -671,10 +671,12 @@ let NotificationFeedItem = ({
|
|||||||
{item.type === 'feedgen-like' && item.subjectUri ? (
|
{item.type === 'feedgen-like' && item.subjectUri ? (
|
||||||
<FeedSourceCard
|
<FeedSourceCard
|
||||||
feedUri={item.subjectUri}
|
feedUri={item.subjectUri}
|
||||||
|
link={false}
|
||||||
style={[
|
style={[
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
a.border,
|
a.border,
|
||||||
|
a.p_md,
|
||||||
styles.feedcard,
|
styles.feedcard,
|
||||||
]}
|
]}
|
||||||
showLikes
|
showLikes
|
||||||
@@ -1000,7 +1002,6 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
feedcard: {
|
feedcard: {
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
paddingVertical: 12,
|
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
},
|
},
|
||||||
addedContainer: {
|
addedContainer: {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import {useMemo} from 'react'
|
import {useMemo} from 'react'
|
||||||
import {
|
import {
|
||||||
DimensionValue,
|
type DimensionValue,
|
||||||
StyleProp,
|
type StyleProp,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
@@ -233,8 +233,7 @@ export function FeedLoadingPlaceholder({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
paddingHorizontal: 12,
|
padding: 16,
|
||||||
paddingVertical: 18,
|
|
||||||
borderTopWidth: showTopBorder ? StyleSheet.hairlineWidth : 0,
|
borderTopWidth: showTopBorder ? StyleSheet.hairlineWidth : 0,
|
||||||
},
|
},
|
||||||
pal.border,
|
pal.border,
|
||||||
@@ -244,7 +243,7 @@ export function FeedLoadingPlaceholder({
|
|||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
width={36}
|
width={36}
|
||||||
height={36}
|
height={36}
|
||||||
style={[styles.avatar, {borderRadius: 6}]}
|
style={[styles.avatar, {borderRadius: 8}]}
|
||||||
/>
|
/>
|
||||||
<View style={[s.flex1]}>
|
<View style={[s.flex1]}>
|
||||||
<LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} />
|
<LoadingPlaceholder width={100} height={8} style={[s.mt5, s.mb10]} />
|
||||||
@@ -252,12 +251,7 @@ export function FeedLoadingPlaceholder({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{showLowerPlaceholder && (
|
{showLowerPlaceholder && (
|
||||||
<View style={{paddingHorizontal: 5, marginTop: 10}}>
|
<View style={{marginTop: 12}}>
|
||||||
<LoadingPlaceholder
|
|
||||||
width={260}
|
|
||||||
height={8}
|
|
||||||
style={{marginVertical: 12}}
|
|
||||||
/>
|
|
||||||
<LoadingPlaceholder width={120} height={8} />
|
<LoadingPlaceholder width={120} height={8} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@@ -352,8 +346,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
marginRight: 10,
|
marginRight: 12,
|
||||||
marginLeft: 8,
|
|
||||||
},
|
},
|
||||||
notification: {
|
notification: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/compon
|
|||||||
import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk'
|
import {FloppyDisk_Stroke2_Corner0_Rounded as SaveIcon} from '#/components/icons/FloppyDisk'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
|
import {Text as NewText} from '#/components/Typography'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
|
type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
|
||||||
export function SavedFeeds({}: Props) {
|
export function SavedFeeds({}: Props) {
|
||||||
@@ -296,7 +297,7 @@ function ListItem({
|
|||||||
<FeedSourceCard
|
<FeedSourceCard
|
||||||
key={feedUri}
|
key={feedUri}
|
||||||
feedUri={feedUri}
|
feedUri={feedUri}
|
||||||
style={[isPinned && {paddingRight: 8}]}
|
style={[isPinned && a.pr_sm]}
|
||||||
showMinimalPlaceholder
|
showMinimalPlaceholder
|
||||||
hideTopBorder={true}
|
hideTopBorder={true}
|
||||||
/>
|
/>
|
||||||
@@ -391,26 +392,17 @@ function ListItem({
|
|||||||
function FollowingFeedCard() {
|
function FollowingFeedCard() {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<View
|
<View style={[a.flex_row, a.align_center, a.flex_1, a.p_lg]}>
|
||||||
style={[
|
|
||||||
a.flex_row,
|
|
||||||
a.align_center,
|
|
||||||
a.flex_1,
|
|
||||||
{
|
|
||||||
paddingHorizontal: 18,
|
|
||||||
paddingVertical: 20,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.justify_center,
|
a.justify_center,
|
||||||
a.rounded_sm,
|
a.rounded_sm,
|
||||||
|
a.mr_md,
|
||||||
{
|
{
|
||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
backgroundColor: t.palette.primary_500,
|
backgroundColor: t.palette.primary_500,
|
||||||
marginRight: 10,
|
|
||||||
},
|
},
|
||||||
]}>
|
]}>
|
||||||
<FilterTimeline
|
<FilterTimeline
|
||||||
@@ -423,11 +415,10 @@ function FollowingFeedCard() {
|
|||||||
fill={t.palette.white}
|
fill={t.palette.white}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View style={[a.flex_1, a.flex_row, a.gap_sm, a.align_center]}>
|
||||||
style={{flex: 1, flexDirection: 'row', gap: 8, alignItems: 'center'}}>
|
<NewText style={[a.text_sm, a.font_bold, a.leading_snug]}>
|
||||||
<Text type="lg-medium" style={[t.atoms.text]} numberOfLines={1}>
|
|
||||||
<Trans>Following</Trans>
|
<Trans>Following</Trans>
|
||||||
</Text>
|
</NewText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user