diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx
index e4891aacb1..ec7a7356ad 100644
--- a/src/components/Divider.tsx
+++ b/src/components/Divider.tsx
@@ -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 (
-
+
)
}
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
index f94692e5bd..33905dacd9 100644
--- a/src/components/FeedCard.tsx
+++ b/src/components/FeedCard.tsx
@@ -214,7 +214,7 @@ export function DescriptionPlaceholder() {
export function Likes({count}: {count: number}) {
const t = useTheme()
return (
-
+
Liked by
diff --git a/src/components/ListCard.tsx b/src/components/ListCard.tsx
index 30156ee0d3..e34830ea9c 100644
--- a/src/components/ListCard.tsx
+++ b/src/components/ListCard.tsx
@@ -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,
+) {
const {view, showPinButton} = props
const moderationOpts = useModerationOpts()
const moderation = moderationOpts
diff --git a/src/components/Post/Embed/FeedEmbed.tsx b/src/components/Post/Embed/FeedEmbed.tsx
index fad4cd4d8b..47d59e3463 100644
--- a/src/components/Post/Embed/FeedEmbed.tsx
+++ b/src/components/Post/Embed/FeedEmbed.tsx
@@ -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 (
-
+
+
+
+
+
+
+
+
+
)
}
@@ -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 (
-
+
)
}
-
-const styles = StyleSheet.create({
- customFeedOuter: {
- borderWidth: StyleSheet.hairlineWidth,
- borderRadius: 8,
- paddingHorizontal: 12,
- paddingVertical: 12,
- },
-})
diff --git a/src/components/Post/Embed/ListEmbed.tsx b/src/components/Post/Embed/ListEmbed.tsx
index 82685d2715..c1450bdcf9 100644
--- a/src/components/Post/Embed/ListEmbed.tsx
+++ b/src/components/Post/Embed/ListEmbed.tsx
@@ -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 (
-
-
-
+
)
}
@@ -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 (
-
+
)
diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx
index ace85dc984..9c5444b27d 100644
--- a/src/components/Post/Embed/index.tsx
+++ b/src/components/Post/Embed/index.tsx
@@ -268,64 +268,60 @@ export function QuoteEmbed({
const [hover, setHover] = React.useState(false)
return (
{
- setHover(true)
- }}
- onPointerLeave={() => {
- setHover(false)
- }}>
+ style={[a.mt_sm]}
+ onPointerEnter={() => setHover(true)}
+ onPointerLeave={() => setHover(false)}>
-
-
-
-
-
- {moderation ? (
-
- ) : null}
- {richText ? (
-
- ) : null}
- {quote.embed && (
-
- )}
-
+ {({active}) => (
+ <>
+ {!active && }
+
+
+
+
+ {moderation ? (
+
+ ) : null}
+ {richText ? (
+
+ ) : null}
+ {quote.embed && (
+
+ )}
+
+ >
+ )}
)
diff --git a/src/components/moderation/ContentHider.tsx b/src/components/moderation/ContentHider.tsx
index 9e94a413c6..549a1b9f0a 100644
--- a/src/components/moderation/ContentHider.tsx
+++ b/src/components/moderation/ContentHider.tsx
@@ -23,20 +23,23 @@ export function ContentHider({
modui,
ignoreMute,
style,
+ activeStyle,
childContainerStyle,
children,
-}: React.PropsWithChildren<{
+}: {
testID?: string
modui: ModerationUI | undefined
ignoreMute?: boolean
style?: StyleProp
+ activeStyle?: StyleProp
childContainerStyle?: StyleProp
-}>) {
+ children?: React.ReactNode | ((props: {active: boolean}) => React.ReactNode)
+}) {
const blur = modui?.blurs[0]
if (!blur || (ignoreMute && isJustAMute(modui))) {
return (
- {children}
+ {typeof children === 'function' ? children({active: false}) : children}
)
}
@@ -44,9 +47,9 @@ export function ContentHider({
- {children}
+ {typeof children === 'function' ? children({active: true}) : children}
)
}
diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx
index 3a658755a8..18e2807a84 100644
--- a/src/view/com/feeds/FeedSourceCard.tsx
+++ b/src/view/com/feeds/FeedSourceCard.tsx
@@ -1,50 +1,33 @@
-import React from 'react'
+import {type StyleProp, View, type ViewStyle} from 'react-native'
import {
- Linking,
- Pressable,
- StyleProp,
- StyleSheet,
- View,
- ViewStyle,
-} from 'react-native'
-import {AtUri} from '@atproto/api'
-import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+ type $Typed,
+ AppBskyFeedDefs,
+ type AppBskyGraphDefs,
+ AtUri,
+} from '@atproto/api'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
-import {useNavigationDeduped} from '#/lib/hooks/useNavigationDeduped'
-import {usePalette} from '#/lib/hooks/usePalette'
import {sanitizeHandle} from '#/lib/strings/handles'
-import {s} from '#/lib/styles'
-import {logger} from '#/logger'
-import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed'
import {
- useAddSavedFeedsMutation,
- usePreferencesQuery,
- UsePreferencesQueryResponse,
- useRemoveFeedMutation,
-} from '#/state/queries/preferences'
+ type FeedSourceInfo,
+ hydrateFeedGenerator,
+ hydrateList,
+ useFeedSourceInfoQuery,
+} from '#/state/queries/feed'
import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
-import * as Toast from '#/view/com/util/Toast'
-import {useTheme} from '#/alf'
-import {atoms as a} from '#/alf'
-import {shouldClickOpenNewTab} from '#/components/Link'
-import * as Prompt from '#/components/Prompt'
+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 '../util/text/Text'
-import {UserAvatar} from '../util/UserAvatar'
+import {Text} from '#/components/Typography'
+import {MissingFeed} from './MissingFeed'
-export function FeedSourceCard({
- feedUri,
- style,
- showSaveBtn = false,
- showDescription = false,
- showLikes = false,
- pinOnSave = false,
- showMinimalPlaceholder,
- hideTopBorder,
-}: {
+type FeedSourceCardProps = {
feedUri: string
+ feedData?:
+ | $Typed
+ | $Typed
style?: StyleProp
showSaveBtn?: boolean
showDescription?: boolean
@@ -52,22 +35,41 @@ export function FeedSourceCard({
pinOnSave?: boolean
showMinimalPlaceholder?: boolean
hideTopBorder?: boolean
-}) {
- const {data: preferences} = usePreferencesQuery()
- const {data: feed} = useFeedSourceInfoQuery({uri: feedUri})
+ 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
+ } else {
+ return
+ }
+}
+
+export function FeedSourceCardWithoutData({
+ feedUri,
+ ...props
+}: Omit) {
+ const {data: feed, error} = useFeedSourceInfoQuery({
+ uri: feedUri,
+ })
return (
)
}
@@ -75,80 +77,26 @@ export function FeedSourceCard({
export function FeedSourceCardLoaded({
feedUri,
feed,
- preferences,
style,
- showSaveBtn = false,
showDescription = false,
showLikes = false,
- pinOnSave = false,
showMinimalPlaceholder,
hideTopBorder,
+ link = true,
+ error,
}: {
feedUri: string
feed?: FeedSourceInfo
- preferences?: UsePreferencesQueryResponse
style?: StyleProp
- showSaveBtn?: boolean
showDescription?: boolean
showLikes?: boolean
- pinOnSave?: boolean
showMinimalPlaceholder?: boolean
hideTopBorder?: boolean
+ link?: boolean
+ error?: unknown
}) {
const t = useTheme()
- const pal = usePalette('default')
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
@@ -156,200 +104,117 @@ export function FeedSourceCardLoaded({
* This state also captures the scenario where a feed can't load for whatever
* reason.
*/
- if (!feed || !preferences)
+ if (!feed) {
+ if (error) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ }
+
+ const inner = (
+ <>
+
+
+
+
+
+
+ {feed.displayName}
+
+
+ {feed.type === 'feed' ? (
+ Feed by {sanitizeHandle(feed.creatorHandle, '@')}
+ ) : (
+ List by {sanitizeHandle(feed.creatorHandle, '@')}
+ )}
+
+
+
+ {showDescription && feed.description ? (
+
+ ) : null}
+ {showLikes && feed.type === 'feed' ? (
+
+
+ Liked by{' '}
+
+
+
+ ) : null}
+ >
+ )
+
+ if (link) {
+ return (
+
+ {inner}
+
+ )
+ } else {
return (
- {showMinimalPlaceholder ? (
-
- ) : (
-
- )}
-
- {showSaveBtn && (
-
-
-
- )}
+ {inner}
)
-
- return (
- <>
- {
- 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}>
-
-
-
-
-
-
- {feed.displayName}
-
-
- {feed.type === 'feed' ? (
- Feed by {sanitizeHandle(feed.creatorHandle, '@')}
- ) : (
- List by {sanitizeHandle(feed.creatorHandle, '@')}
- )}
-
-
-
- {showSaveBtn && (
-
-
- {isSaved ? (
-
- ) : (
-
- )}
-
-
- )}
-
-
- {showDescription && feed.description ? (
-
- ) : null}
-
- {showLikes && feed.type === 'feed' ? (
-
-
- Liked by{' '}
-
-
-
- ) : null}
-
-
-
- >
- )
+ }
}
-
-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,
- },
-})
diff --git a/src/view/com/feeds/MissingFeed.tsx b/src/view/com/feeds/MissingFeed.tsx
new file mode 100644
index 0000000000..3d281a7316
--- /dev/null
+++ b/src/view/com/feeds/MissingFeed.tsx
@@ -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
+ hideTopBorder?: boolean
+ uri: string
+ error?: unknown
+}) {
+ const t = useTheme()
+ const {_} = useLingui()
+ const control = Dialog.useDialogControl()
+
+ const type = getFeedTypeFromUri(uri)
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+
+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 (
+
+
+
+ {type === 'feed' ? (
+ Could not connect to feed service
+ ) : (
+ Deleted list
+ )}
+
+
+ {type === 'feed' ? (
+
+ We could not connect to the service that provides this custom
+ feed. It may be temporarily unavailable and experiencing issues,
+ or permanently unavailable.
+
+ ) : (
+ We could not find this list. It was probably deleted.
+ )}
+
+
+
+ {type === 'feed' ? (
+ Feed creator
+ ) : (
+ List creator
+ )}
+
+ {profile && moderationOpts && (
+
+ control.close()}>
+
+
+
+
+
+
+ )}
+ {isProfileError && (
+
+ Could not find profile
+
+ )}
+ {type === 'feed' && (
+ <>
+
+ Feed identifier
+
+
+ {atUri.rkey}
+
+ >
+ )}
+ {error instanceof Error && (
+ <>
+
+ Error message
+
+
+ {cleanError(error.message)}
+
+ >
+ )}
+
+ {isNative && (
+
+ )}
+
+
+ )
+}
diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx
index 89e2d20e76..4de21e5985 100644
--- a/src/view/com/notifications/NotificationFeedItem.tsx
+++ b/src/view/com/notifications/NotificationFeedItem.tsx
@@ -671,10 +671,12 @@ let NotificationFeedItem = ({
{item.type === 'feedgen-like' && item.subjectUri ? (
@@ -252,12 +251,7 @@ export function FeedLoadingPlaceholder({
{showLowerPlaceholder && (
-
-
+
)}
@@ -352,8 +346,7 @@ const styles = StyleSheet.create({
},
avatar: {
borderRadius: 999,
- marginRight: 10,
- marginLeft: 8,
+ marginRight: 12,
},
notification: {
flexDirection: 'row',
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 9ae3dbd8d8..b244c75588 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -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 * as Layout from '#/components/Layout'
import {Loader} from '#/components/Loader'
+import {Text as NewText} from '#/components/Typography'
type Props = NativeStackScreenProps
export function SavedFeeds({}: Props) {
@@ -296,7 +297,7 @@ function ListItem({
@@ -391,26 +392,17 @@ function ListItem({
function FollowingFeedCard() {
const t = useTheme()
return (
-
+
-
-
+
+
Following
-
+
)