Skip to content

Commit

Permalink
feat: add entry list
Browse files Browse the repository at this point in the history
  • Loading branch information
lawvs committed Jan 22, 2025
1 parent 6fae018 commit c052749
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 97 deletions.
2 changes: 1 addition & 1 deletion apps/mobile/src/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ let db: ExpoSQLiteDatabase<typeof schema> & {
export function initializeDb() {
db = drizzle(sqlite, {
schema,
logger: true,
logger: false,
})
}

Expand Down
40 changes: 40 additions & 0 deletions apps/mobile/src/modules/entry-list/action.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Link } from "expo-router"
import { TouchableOpacity, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"

import { AddCuteReIcon } from "@/src/icons/add_cute_re"
import { LayoutLeftbarOpenCuteReIcon } from "@/src/icons/layout_leftbar_open_cute_re"
import { accentColor } from "@/src/theme/colors"

import { useFeedDrawer } from "../feed-drawer/atoms"

const useActionPadding = () => {
const insets = useSafeAreaInsets()
return { paddingLeft: insets.left + 12, paddingRight: insets.right + 12 }
}

export function LeftAction() {
const { openDrawer } = useFeedDrawer()

const insets = useActionPadding()

return (
<TouchableOpacity onPress={openDrawer} style={{ paddingLeft: insets.paddingLeft }}>
<LayoutLeftbarOpenCuteReIcon color={accentColor} />
</TouchableOpacity>
)
}

export function RightAction() {
const insets = useActionPadding()

return (
<View className="flex-row items-center gap-4" style={{ paddingRight: insets.paddingRight }}>
<Link asChild href="/add">
<TouchableOpacity className="size-6">
<AddCuteReIcon color={accentColor} />
</TouchableOpacity>
</Link>
</View>
)
}
163 changes: 163 additions & 0 deletions apps/mobile/src/modules/entry-list/entry-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import type { FeedViewType } from "@follow/constants"
import { useIsFocused } from "@react-navigation/native"
import { router, Stack } from "expo-router"
import { useCallback, useEffect } from "react"
import { Image, Text, View } from "react-native"

import { BlurEffect } from "@/src/components/common/BlurEffect"
import { SafeNavigationScrollView } from "@/src/components/common/SafeNavigationScrollView"
import { ItemPressable } from "@/src/components/ui/pressable/item-pressable"
import {
useSelectedFeed,
useSetDrawerSwipeDisabled,
useViewDefinition,
} from "@/src/modules/feed-drawer/atoms"
import {
useEntry,
useEntryIdsByCategory,
useEntryIdsByFeedId,
useEntryIdsByView,
} from "@/src/store/entry/hooks"
import { useFeed } from "@/src/store/feed/hooks"
import { useList } from "@/src/store/list/hooks"

import { LeftAction, RightAction } from "./action"

export function EntryList() {
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const isFocused = useIsFocused()
useEffect(() => {
if (isFocused) {
setDrawerSwipeDisabled(false)
} else {
setDrawerSwipeDisabled(true)
}
}, [setDrawerSwipeDisabled, isFocused])

const selectedFeed = useSelectedFeed()

switch (selectedFeed.type) {
case "view": {
return <ViewEntryList viewId={selectedFeed.viewId} />
}
case "feed": {
return <FeedEntryList feedId={selectedFeed.feedId} />
}
case "category": {
return <CategoryEntryList categoryName={selectedFeed.categoryName} />
}
case "list": {
return <ListEntryList listId={selectedFeed.listId} />
}
// case "inbox": {
// return <InboxEntryList inboxId={selectedFeed.inboxId} />
// }
// No default
}
}

function ViewEntryList({ viewId }: { viewId: FeedViewType }) {
const entryIds = useEntryIdsByView(viewId)
const viewDef = useViewDefinition(viewId)
return <EntryListScreen title={viewDef.name} entryIds={entryIds} />
}

function FeedEntryList({ feedId }: { feedId: string }) {
const feed = useFeed(feedId)
const entryIds = useEntryIdsByFeedId(feedId)
return <EntryListScreen title={feed?.title ?? ""} entryIds={entryIds} />
}

function CategoryEntryList({ categoryName }: { categoryName: string }) {
const entryIds = useEntryIdsByCategory(categoryName)
return <EntryListScreen title={categoryName} entryIds={entryIds} />
}

function ListEntryList({ listId }: { listId: string }) {
const list = useList(listId)
if (!list) return null

return <EntryListScreen title={list.title} entryIds={list.entryIds ?? []} />
}

function EntryListScreen({ title, entryIds }: { title: string; entryIds: string[] }) {
return (
<>
<Stack.Screen
options={{
headerShown: true,
headerTitle: title,
headerLeft: LeftAction,
headerRight: RightAction,
headerTransparent: true,
headerBackground: BlurEffect,
}}
/>

<SafeNavigationScrollView contentInsetAdjustmentBehavior="automatic" withTopInset>
<View className="flex">
{entryIds.map((id) => (
<EntryItem key={id} entryId={id} />
))}
</View>
</SafeNavigationScrollView>
</>
)
}

function EntryItem({ entryId }: { entryId: string }) {
const entry = useEntry(entryId)

const handlePress = useCallback(() => {
router.push({
pathname: `/feeds/[feedId]`,
params: {
feedId: entryId,
},
})
}, [entryId])

if (!entry) return <EntryItemSkeleton />
const { title, description, publishedAt, media } = entry
const image = media?.[0]?.url

return (
<ItemPressable
className="bg-system-background flex flex-row items-center p-4"
onPress={handlePress}
>
<View className="flex-1 space-y-2">
<Text numberOfLines={2} className="text-lg font-semibold text-zinc-900 dark:text-zinc-100">
{title}
</Text>
<Text className="line-clamp-2 text-sm text-zinc-600 dark:text-zinc-400">{description}</Text>
<Text className="text-xs text-zinc-500 dark:text-zinc-500">
{publishedAt.toLocaleString()}
</Text>
</View>
{image && (
<Image
source={{ uri: image }}
className="ml-2 size-20 rounded-md bg-zinc-200 dark:bg-zinc-800"
resizeMode="cover"
/>
)}
</ItemPressable>
)
}

function EntryItemSkeleton() {
return (
<View className="bg-system-background flex flex-row items-center p-4">
<View className="flex flex-1 flex-col justify-between">
{/* Title skeleton */}
<View className="h-6 w-3/4 animate-pulse rounded-md bg-zinc-200 dark:bg-zinc-800" />
{/* Description skeleton */}
<View className="mt-2 w-full flex-1 animate-pulse rounded-md bg-zinc-200 dark:bg-zinc-800" />
</View>

{/* Image skeleton */}
<View className="ml-2 size-20 animate-pulse rounded-md bg-zinc-200 dark:bg-zinc-800" />
</View>
)
}
66 changes: 66 additions & 0 deletions apps/mobile/src/modules/feed-drawer/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"
import { useCallback, useMemo } from "react"

import { views } from "@/src/constants/views"
import { usePrefetchEntries } from "@/src/store/entry/hooks"
import type { FetchEntriesProps } from "@/src/store/entry/types"

// drawer open state

Expand All @@ -20,6 +22,8 @@ export function useFeedDrawer() {
}
}

export const closeDrawer = () => jotaiStore.set(drawerOpenAtom, false)

// is drawer swipe disabled

const isDrawerSwipeDisabledAtom = atom<boolean>(true)
Expand Down Expand Up @@ -54,6 +58,68 @@ export function useSelectedCollection() {
}
export const selectCollection = (state: CollectionPanelState) => {
jotaiStore.set(collectionPanelStateAtom, state)
if (state.type === "view") {
jotaiStore.set(selectedFeedAtom, state)
}
}

// feed panel selected state

export type SelectedFeed =
| {
type: "view"
viewId: FeedViewType
}
| {
type: "feed"
feedId: string
}
| {
type: "category"
categoryName: string
}
| {
type: "list"
listId: string
}

const selectedFeedAtom = atom<SelectedFeed>({
type: "view",
viewId: FeedViewType.Articles,
})

export function useSelectedFeed() {
const selectedFeed = useAtomValue(selectedFeedAtom)
let payload: FetchEntriesProps = {}
switch (selectedFeed.type) {
case "view": {
payload = { view: selectedFeed.viewId }
break
}
case "feed": {
payload = { feedId: selectedFeed.feedId }
break
}
case "category": {
// payload = { category: selectedFeed.categoryName }
break
}
case "list": {
payload = { listId: selectedFeed.listId }
break
}
// case "inbox": {
// payload = { inboxId: selectedFeed.inboxId }
// break
// }
// No default
}
usePrefetchEntries(payload)
return selectedFeed
}

export const selectFeed = (state: SelectedFeed) => {
jotaiStore.set(selectedFeedAtom, state)
}

export const useViewDefinition = (view: FeedViewType) => {
Expand Down
16 changes: 8 additions & 8 deletions apps/mobile/src/modules/feed-drawer/feed-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { cn } from "@follow/utils"
import { BottomTabBarHeightContext } from "@react-navigation/bottom-tabs"
import { HeaderHeightContext } from "@react-navigation/elements"
import { router } from "expo-router"
import type { FC } from "react"
import { createContext, memo, useContext, useState } from "react"
import {
Expand Down Expand Up @@ -33,7 +32,7 @@ import {
import { useCurrentView, useFeedListSortMethod, useFeedListSortOrder } from "../subscription/atoms"
import { ViewPageCurrentViewProvider } from "../subscription/ctx"
import { SubscriptionList } from "../subscription/SubscriptionLists"
import { useSelectedCollection } from "./atoms"
import { selectFeed, useSelectedCollection } from "./atoms"
import { ListHeaderComponent, ViewHeaderComponent } from "./header"

export const FeedPanel = () => {
Expand Down Expand Up @@ -150,7 +149,10 @@ const CategoryGrouped = memo(
>
<ItemPressable
onPress={() => {
// TODO navigate to category
selectFeed({
type: "category",
categoryName: category,
})
}}
className="h-12 flex-row items-center px-3"
>
Expand Down Expand Up @@ -223,11 +225,9 @@ const SubscriptionItem = memo(({ id, className }: { id: string; className?: stri
className,
)}
onPress={() => {
router.push({
pathname: `/feeds/[feedId]`,
params: {
feedId: id,
},
selectFeed({
type: "feed",
feedId: id,
})
}}
>
Expand Down
7 changes: 6 additions & 1 deletion apps/mobile/src/modules/subscription/CategoryGrouped.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useUnreadCounts } from "@/src/store/unread/hooks"
import { useColor } from "@/src/theme/colors"

import { SubscriptionFeedCategoryContextMenu } from "../context-menu/feeds"
import { closeDrawer, selectFeed } from "../feed-drawer/atoms"
import { GroupedContext, useViewPageCurrentView } from "./ctx"
import { ItemSeparator } from "./ItemSeparator"
import { UnGroupedList } from "./UnGroupedList"
Expand Down Expand Up @@ -42,7 +43,11 @@ export const CategoryGrouped = memo(
>
<ItemPressable
onPress={() => {
// TODO navigate to category
selectFeed({
type: "category",
categoryName: category,
})
closeDrawer()
}}
className="h-12 flex-row items-center px-3"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useList } from "@/src/store/list/hooks"
import { useUnreadCount } from "@/src/store/unread/hooks"

import { SubscriptionListItemContextMenu } from "../../context-menu/lists"
import { closeDrawer, selectFeed } from "../../feed-drawer/atoms"

export const ListSubscriptionItem = memo(({ id }: { id: string; className?: string }) => {
const list = useList(id)
Expand All @@ -16,7 +17,16 @@ export const ListSubscriptionItem = memo(({ id }: { id: string; className?: stri
return (
<Animated.View exiting={FadeOutUp}>
<SubscriptionListItemContextMenu id={id}>
<ItemPressable className="h-12 flex-row items-center px-3">
<ItemPressable
className="h-12 flex-row items-center px-3"
onPress={() => {
selectFeed({
type: "list",
listId: id,
})
closeDrawer()
}}
>
<View className="overflow-hidden rounded">
{!!list.image && (
<Image source={{ uri: list.image, width: 24, height: 24 }} resizeMode="cover" />
Expand Down
Loading

0 comments on commit c052749

Please sign in to comment.