diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
new file mode 100644
index 0000000000..0300a07781
--- /dev/null
+++ b/src/components/FeedCard.tsx
@@ -0,0 +1,219 @@
+import React from 'react'
+import {GestureResponderEvent, View} from 'react-native'
+import {AppBskyActorDefs, AppBskyFeedDefs, AtUri} from '@atproto/api'
+import {msg, plural, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {logger} from '#/logger'
+import {
+ useAddSavedFeedsMutation,
+ usePreferencesQuery,
+ useRemoveFeedMutation,
+} from '#/state/queries/preferences'
+import {sanitizeHandle} from 'lib/strings/handles'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import * as Toast from 'view/com/util/Toast'
+import {useTheme} from '#/alf'
+import {atoms as a} from '#/alf'
+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 {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}) {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export function Link({
+ children,
+ feed,
+}: {
+ children: React.ReactElement
+ feed: AppBskyFeedDefs.GeneratorView
+}) {
+ const href = React.useMemo(() => {
+ const urip = new AtUri(feed.uri)
+ const handleOrDid = feed.creator.handle || feed.creator.did
+ return `/profile/${handleOrDid}/feed/${urip.rkey}`
+ }, [feed])
+ return {children}
+}
+
+export function Outer({children}: {children: React.ReactNode}) {
+ return {children}
+}
+
+export function Header({children}: {children: React.ReactNode}) {
+ return {children}
+}
+
+export function Avatar({src}: {src: string | undefined}) {
+ return
+}
+
+export function TitleAndByline({
+ title,
+ creator,
+}: {
+ title: string
+ creator: AppBskyActorDefs.ProfileViewBasic
+}) {
+ const t = useTheme()
+
+ return (
+
+
+ {title}
+
+
+ Feed by {sanitizeHandle(creator.handle, '@')}
+
+
+ )
+}
+
+export function Description({description}: {description?: string}) {
+ const t = useTheme()
+ const [rt, isResolving] = useRichText(description || '')
+ if (!description) return null
+ return isResolving ? (
+
+
+
+
+ ) : (
+
+ )
+}
+
+export function Likes({count}: {count: number}) {
+ const t = useTheme()
+ return (
+
+ {plural(count || 0, {
+ one: 'Liked by # user',
+ other: 'Liked by # users',
+ })}
+
+ )
+}
+
+export function Action({uri, pin}: {uri: string; pin?: boolean}) {
+ const {_} = useLingui()
+ const {data: preferences} = usePreferencesQuery()
+ const {isPending: isAddSavedFeedPending, mutateAsync: saveFeeds} =
+ useAddSavedFeedsMutation()
+ const {isPending: isRemovePending, mutateAsync: removeFeed} =
+ useRemoveFeedMutation()
+ const savedFeedConfig = React.useMemo(() => {
+ return preferences?.savedFeeds?.find(
+ feed => feed.type === 'feed' && feed.value === uri,
+ )
+ }, [preferences?.savedFeeds, uri])
+ const removePromptControl = Prompt.usePromptControl()
+ const isPending = isAddSavedFeedPending || isRemovePending
+
+ const toggleSave = React.useCallback(
+ async (e: GestureResponderEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ try {
+ if (savedFeedConfig) {
+ await removeFeed(savedFeedConfig)
+ } else {
+ await saveFeeds([
+ {
+ type: 'feed',
+ value: uri,
+ pinned: pin || false,
+ },
+ ])
+ }
+ Toast.show(_(msg`Feeds updated!`))
+ } catch (e: any) {
+ logger.error(e, {context: `FeedCard: failed to update feeds`, pin})
+ Toast.show(_(msg`Failed to update feeds`))
+ }
+ },
+ [_, pin, saveFeeds, removeFeed, uri, savedFeedConfig],
+ )
+
+ const onPrompRemoveFeed = React.useCallback(
+ async (e: GestureResponderEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ removePromptControl.open()
+ },
+ [removePromptControl],
+ )
+
+ return (
+ <>
+
+
+
+ >
+ )
+}
diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx
index 612559455f..e4091d0f78 100644
--- a/src/view/screens/Feeds.tsx
+++ b/src/view/screens/Feeds.tsx
@@ -1,6 +1,6 @@
import React from 'react'
import {ActivityIndicator, type FlatList, StyleSheet, View} from 'react-native'
-import {AppBskyActorDefs} from '@atproto/api'
+import {AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome'
import {msg, Trans} from '@lingui/macro'
@@ -25,7 +25,6 @@ import {ComposeIcon2} from 'lib/icons'
import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {cleanError} from 'lib/strings/errors'
import {s} from 'lib/styles'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {FAB} from 'view/com/util/fab/FAB'
import {SearchInput} from 'view/com/util/forms/SearchInput'
@@ -46,6 +45,8 @@ import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/compon
import {ListMagnifyingGlass_Stroke2_Corner0_Rounded} from '#/components/icons/ListMagnifyingGlass'
import {ListSparkle_Stroke2_Corner0_Rounded} from '#/components/icons/ListSparkle'
import hairlineWidth = StyleSheet.hairlineWidth
+import {Divider} from '#/components/Divider'
+import * as FeedCard from '#/components/FeedCard'
type Props = NativeStackScreenProps
@@ -94,6 +95,7 @@ type FlatlistSlice =
type: 'popularFeed'
key: string
feedUri: string
+ feed: AppBskyFeedDefs.GeneratorView
}
| {
type: 'popularFeedsLoadingMore'
@@ -300,6 +302,7 @@ export function FeedsScreen(_props: Props) {
key: `popularFeed:${feed.uri}`,
type: 'popularFeed',
feedUri: feed.uri,
+ feed,
})),
)
}
@@ -323,6 +326,7 @@ export function FeedsScreen(_props: Props) {
key: `popularFeed:${feed.uri}`,
type: 'popularFeed',
feedUri: feed.uri,
+ feed,
})),
)
}
@@ -476,13 +480,10 @@ export function FeedsScreen(_props: Props) {
return
} else if (item.type === 'popularFeed') {
return (
-
+
+
+
+
)
} else if (item.type === 'popularFeedsNoResults') {
return (
@@ -525,7 +526,6 @@ export function FeedsScreen(_props: Props) {
onPressCancelSearch,
onSubmitQuery,
onChangeSearchFocus,
- hasSession,
],
)