diff --git a/apps/mobile/src/modules/context-menu/entry.tsx b/apps/mobile/src/modules/context-menu/entry.tsx
index 28a95bcef..9b21ea915 100644
--- a/apps/mobile/src/modules/context-menu/entry.tsx
+++ b/apps/mobile/src/modules/context-menu/entry.tsx
@@ -54,7 +54,7 @@ export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: s
unreadSyncService.markEntryAsRead(id)
}}
>
- Mark As Read
+ Mark as Read
- Open link
+ Open Link
)}
diff --git a/apps/mobile/src/modules/entry-list/ItemSeparator.tsx b/apps/mobile/src/modules/entry-list/ItemSeparator.tsx
new file mode 100644
index 000000000..eef10a98a
--- /dev/null
+++ b/apps/mobile/src/modules/entry-list/ItemSeparator.tsx
@@ -0,0 +1,9 @@
+import { StyleSheet, View } from "react-native"
+
+const el = (
+
+)
+
+export const ItemSeparator = () => {
+ return el
+}
diff --git a/apps/mobile/src/modules/entry-list/entry-list-article.tsx b/apps/mobile/src/modules/entry-list/entry-list-article.tsx
new file mode 100644
index 000000000..c8cb0d5a3
--- /dev/null
+++ b/apps/mobile/src/modules/entry-list/entry-list-article.tsx
@@ -0,0 +1,135 @@
+import type { ListRenderItemInfo } from "@shopify/flash-list"
+import { Image } from "expo-image"
+import { router } from "expo-router"
+import { useCallback, useEffect, useMemo } from "react"
+import { Animated, Text, View } from "react-native"
+import { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
+
+import { setWebViewEntry } from "@/src/components/native/webview/EntryContentWebView"
+import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
+import { gentleSpringPreset } from "@/src/constants/spring"
+import { useEntryListContext, useFetchEntriesControls } from "@/src/modules/feed-drawer/atoms"
+import { useEntry } from "@/src/store/entry/hooks"
+import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store"
+
+import { EntryItemContextMenu } from "../context-menu/entry"
+import { TimelineSelectorList } from "../screen/TimelineSelectorList"
+import { LoadArchiveButton } from "./action"
+import { ItemSeparator } from "./ItemSeparator"
+
+export function EntryListContentArticle({ entryIds }: { entryIds: string[] }) {
+ const screenType = useEntryListContext().type
+
+ const { fetchNextPage, isFetching, refetch, isRefetching } = useFetchEntriesControls()
+
+ const renderItem = useCallback(
+ ({ item: id }: ListRenderItemInfo) => ,
+ [],
+ )
+
+ const ListFooterComponent = useMemo(
+ () =>
+ isFetching ? : screenType === "feed" ? : null,
+ [isFetching, screenType],
+ )
+
+ return (
+ {
+ refetch()
+ }}
+ isRefetching={isRefetching}
+ data={entryIds}
+ renderItem={renderItem}
+ onEndReached={() => {
+ fetchNextPage()
+ }}
+ onViewableItemsChanged={({ viewableItems }) => {
+ debouncedFetchEntryContentByStream(viewableItems.map((item) => item.key))
+ }}
+ estimatedItemSize={100}
+ ItemSeparatorComponent={ItemSeparator}
+ ListFooterComponent={ListFooterComponent}
+ />
+ )
+}
+
+function EntryItem({ entryId }: { entryId: string }) {
+ const entry = useEntry(entryId)
+
+ const handlePress = useCallback(() => {
+ if (!entry) return
+ setWebViewEntry(entry)
+ router.push(`/entries/${entryId}`)
+ }, [entryId, entry])
+
+ const unreadZoomSharedValue = useSharedValue(entry?.read ? 0 : 1)
+
+ const unreadIndicatorStyle = useAnimatedStyle(() => {
+ return {
+ transform: [
+ {
+ scale: unreadZoomSharedValue.value,
+ },
+ ],
+ }
+ })
+
+ useEffect(() => {
+ if (!entry) return
+
+ if (entry.read) {
+ unreadZoomSharedValue.value = withSpring(0, gentleSpringPreset)
+ } else {
+ unreadZoomSharedValue.value = withSpring(1, gentleSpringPreset)
+ }
+ }, [entry, entry?.read, unreadZoomSharedValue])
+
+ if (!entry) return
+ const { title, description, publishedAt, media } = entry
+ const image = media?.[0]?.url
+ const blurhash = media?.[0]?.blurhash
+
+ return (
+
+
+
+
+
+
+ {title}
+
+ {description}
+ {publishedAt.toLocaleString()}
+
+ {image && (
+
+ )}
+
+
+ )
+}
+
+function EntryItemSkeleton() {
+ return (
+
+
+ {/* Title skeleton */}
+
+ {/* Description skeleton */}
+
+
+
+ {/* Image skeleton */}
+
+
+ )
+}
diff --git a/apps/mobile/src/modules/entry-list/entry-list-social.tsx b/apps/mobile/src/modules/entry-list/entry-list-social.tsx
new file mode 100644
index 000000000..1389a7299
--- /dev/null
+++ b/apps/mobile/src/modules/entry-list/entry-list-social.tsx
@@ -0,0 +1,128 @@
+import type { ListRenderItemInfo } from "@shopify/flash-list"
+import { Image } from "expo-image"
+import { router } from "expo-router"
+import { useCallback, useMemo } from "react"
+import { Text, View } from "react-native"
+
+import { setWebViewEntry } from "@/src/components/native/webview/EntryContentWebView"
+import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
+import { useEntryListContext, useFetchEntriesControls } from "@/src/modules/feed-drawer/atoms"
+import { useEntry } from "@/src/store/entry/hooks"
+import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store"
+
+import { EntryItemContextMenu } from "../context-menu/entry"
+import { TimelineSelectorList } from "../screen/TimelineSelectorList"
+import { LoadArchiveButton } from "./action"
+import { ItemSeparator } from "./ItemSeparator"
+
+export function EntryListContentSocial({ entryIds }: { entryIds: string[] }) {
+ const screenType = useEntryListContext().type
+
+ const { fetchNextPage, isFetching, refetch, isRefetching } = useFetchEntriesControls()
+
+ const renderItem = useCallback(
+ ({ item: id }: ListRenderItemInfo) => ,
+ [],
+ )
+
+ const ListFooterComponent = useMemo(
+ () =>
+ isFetching ? : screenType === "feed" ? : null,
+ [isFetching, screenType],
+ )
+
+ return (
+ {
+ refetch()
+ }}
+ isRefetching={isRefetching}
+ data={entryIds}
+ renderItem={renderItem}
+ onEndReached={() => {
+ fetchNextPage()
+ }}
+ onViewableItemsChanged={({ viewableItems }) => {
+ debouncedFetchEntryContentByStream(viewableItems.map((item) => item.key))
+ }}
+ estimatedItemSize={100}
+ ItemSeparatorComponent={ItemSeparator}
+ ListFooterComponent={ListFooterComponent}
+ />
+ )
+}
+
+function EntryItem({ entryId }: { entryId: string }) {
+ const entry = useEntry(entryId)
+
+ const handlePress = useCallback(() => {
+ if (!entry) return
+ setWebViewEntry(entry)
+ router.push(`/entries/${entryId}`)
+ }, [entryId, entry])
+
+ if (!entry) return
+ const { description, publishedAt, media } = entry
+
+ return (
+
+
+
+
+ {entry.author}
+ {/* TODO relative time */}
+ {publishedAt.toLocaleString()}
+
+
+
+ {description}
+
+
+ {media && media.length > 0 && (
+
+ {media.map((image) => {
+ return (
+
+ )
+ })}
+
+ )}
+
+
+ )
+}
+
+function EntryItemSkeleton() {
+ return (
+
+ {/* Header row with avatar, author, and date */}
+
+
+
+
+
+
+ {/* Description area */}
+
+
+
+
+
+ {/* Media preview area */}
+
+
+
+
+
+ )
+}
diff --git a/apps/mobile/src/modules/entry-list/entry-list.tsx b/apps/mobile/src/modules/entry-list/entry-list.tsx
index 039525857..a1e132925 100644
--- a/apps/mobile/src/modules/entry-list/entry-list.tsx
+++ b/apps/mobile/src/modules/entry-list/entry-list.tsx
@@ -1,161 +1,35 @@
import { FeedViewType } from "@follow/constants"
-import type { ListRenderItemInfo } from "@shopify/flash-list"
-import { Image } from "expo-image"
-import { router } from "expo-router"
-import { useCallback, useEffect, useMemo } from "react"
-import { StyleSheet, Text, View } from "react-native"
-import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
-import { setWebViewEntry } from "@/src/components/native/webview/EntryContentWebView"
-import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
-import { gentleSpringPreset } from "@/src/constants/spring"
-import { EntryItemContextMenu } from "@/src/modules/context-menu/entry"
-import { LoadArchiveButton } from "@/src/modules/entry-list/action"
import { EntryListContentGrid } from "@/src/modules/entry-list/entry-list-gird"
-import {
- useEntryListContext,
- useFetchEntriesControls,
- useSelectedView,
-} from "@/src/modules/screen/atoms"
-import { TimelineSelectorHeader } from "@/src/modules/screen/TimelineSelectorHeader"
-import { TimelineSelectorList } from "@/src/modules/screen/TimelineSelectorList"
-import { useEntry } from "@/src/store/entry/hooks"
-import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store"
+import { useSelectedView } from "@/src/modules/screen/atoms"
+
+import { TimelineSelectorHeader } from "../screen/TimelineSelectorHeader"
+import { EntryListContentArticle } from "./entry-list-article"
+import { EntryListContentSocial } from "./entry-list-social"
export function EntryListScreen({ entryIds }: { entryIds: string[] }) {
const view = useSelectedView()
+ let ContentComponent = EntryListContentArticle
+ switch (view) {
+ case FeedViewType.SocialMedia: {
+ ContentComponent = EntryListContentSocial
+ break
+ }
+ case FeedViewType.Pictures:
+ case FeedViewType.Videos: {
+ ContentComponent = EntryListContentGrid
+ break
+ }
+ case FeedViewType.Articles: {
+ ContentComponent = EntryListContentArticle
+ break
+ }
+ }
+
return (
- {view === FeedViewType.Pictures || view === FeedViewType.Videos ? (
-
- ) : (
-
- )}
+
)
}
-
-function EntryListContent({ entryIds }: { entryIds: string[] }) {
- const screenType = useEntryListContext().type
-
- const { fetchNextPage, isFetching, refetch, isRefetching } = useFetchEntriesControls()
-
- const renderItem = useCallback(
- ({ item: id }: ListRenderItemInfo) => ,
- [],
- )
-
- const ListFooterComponent = useMemo(
- () =>
- isFetching ? : screenType === "feed" ? : null,
- [isFetching, screenType],
- )
-
- return (
- {
- refetch()
- }}
- isRefetching={isRefetching}
- data={entryIds}
- renderItem={renderItem}
- onEndReached={() => {
- fetchNextPage()
- }}
- onViewableItemsChanged={({ viewableItems }) => {
- debouncedFetchEntryContentByStream(viewableItems.map((item) => item.key))
- }}
- ItemSeparatorComponent={ItemSeparator}
- ListFooterComponent={ListFooterComponent}
- />
- )
-}
-
-const ItemSeparator = () => {
- return (
-
- )
-}
-function EntryItem({ entryId }: { entryId: string }) {
- const entry = useEntry(entryId)
-
- const handlePress = useCallback(() => {
- if (!entry) return
- setWebViewEntry(entry)
- router.push(`/entries/${entryId}`)
- }, [entryId, entry])
-
- const unreadZoomSharedValue = useSharedValue(entry?.read ? 0 : 1)
-
- const unreadIndicatorStyle = useAnimatedStyle(() => {
- return {
- transform: [
- {
- scale: unreadZoomSharedValue.value,
- },
- ],
- }
- })
- useEffect(() => {
- if (!entry) return
-
- if (entry.read) {
- unreadZoomSharedValue.value = withSpring(0, gentleSpringPreset)
- } else {
- unreadZoomSharedValue.value = withSpring(1, gentleSpringPreset)
- }
- }, [entry?.read, unreadZoomSharedValue])
- if (!entry) return
- const { title, description, publishedAt, media } = entry
- const image = media?.[0]?.url
- const blurhash = media?.[0]?.blurhash
-
- return (
-
-
-
-
-
-
- {title}
-
- {description}
- {publishedAt.toLocaleString()}
-
- {image && (
-
- )}
-
-
- )
-}
-
-function EntryItemSkeleton() {
- return (
-
-
- {/* Title skeleton */}
-
- {/* Description skeleton */}
-
-
-
- {/* Image skeleton */}
-
-
- )
-}