From 676e8bc55b8ec9649035b7812368fb437847d64a Mon Sep 17 00:00:00 2001 From: DIYgod Date: Thu, 13 Feb 2025 13:58:46 +0800 Subject: [PATCH] feat: subscription list; TimelineSelectorHeader and TimelineSelectorList components --- .../mobile/src/modules/context-menu/entry.tsx | 3 +- .../mobile/src/modules/context-menu/feeds.tsx | 2 +- apps/mobile/src/modules/entry-list/action.tsx | 44 +-- .../modules/entry-list/entry-list-gird.tsx | 3 +- .../src/modules/entry-list/entry-list.tsx | 101 ++----- apps/mobile/src/modules/entry-list/index.tsx | 2 +- apps/mobile/src/modules/screen/action.tsx | 40 +++ apps/mobile/src/modules/screen/atoms.ts | 250 ++++++++++++++++++ .../modules/screen/hooks/useHeaderHeight.tsx | 16 ++ .../screen/timeline-selector-header.tsx | 40 +++ .../modules/screen/timeline-selector-list.tsx | 67 +++++ .../{feed-drawer => screen}/view-selector.tsx | 3 +- .../modules/subscription/CategoryGrouped.tsx | 11 +- .../subscription/SubscriptionLists.tsx | 152 ++--------- apps/mobile/src/modules/subscription/ctx.ts | 4 +- .../modules/subscription/items/InboxItem.tsx | 3 +- .../items/ListSubscriptionItem.tsx | 2 +- .../subscription/items/SubscriptionItem.tsx | 8 +- .../screens/(stack)/(tabs)/subscriptions.tsx | 13 +- .../screens/(stack)/feeds/[feedId]/index.tsx | 2 +- apps/mobile/src/store/subscription/hooks.ts | 4 +- 21 files changed, 481 insertions(+), 289 deletions(-) create mode 100644 apps/mobile/src/modules/screen/action.tsx create mode 100644 apps/mobile/src/modules/screen/atoms.ts create mode 100644 apps/mobile/src/modules/screen/hooks/useHeaderHeight.tsx create mode 100644 apps/mobile/src/modules/screen/timeline-selector-header.tsx create mode 100644 apps/mobile/src/modules/screen/timeline-selector-list.tsx rename apps/mobile/src/modules/{feed-drawer => screen}/view-selector.tsx (98%) diff --git a/apps/mobile/src/modules/context-menu/entry.tsx b/apps/mobile/src/modules/context-menu/entry.tsx index 5ad35e0cb8..28a95bcef9 100644 --- a/apps/mobile/src/modules/context-menu/entry.tsx +++ b/apps/mobile/src/modules/context-menu/entry.tsx @@ -11,13 +11,12 @@ import { } from "@/src/components/native/webview/EntryContentWebView" import { openLink } from "@/src/lib/native" import { toast } from "@/src/lib/toast" +import { useSelectedView } from "@/src/modules/screen/atoms" import { useIsEntryStarred } from "@/src/store/collection/hooks" import { collectionSyncService } from "@/src/store/collection/store" import { useEntry } from "@/src/store/entry/hooks" import { unreadSyncService } from "@/src/store/unread/store" -import { useSelectedView } from "../feed-drawer/atoms" - export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: string }>) => { const entry = useEntry(id) const view = useSelectedView() diff --git a/apps/mobile/src/modules/context-menu/feeds.tsx b/apps/mobile/src/modules/context-menu/feeds.tsx index 7f71088126..f8d867d3b2 100644 --- a/apps/mobile/src/modules/context-menu/feeds.tsx +++ b/apps/mobile/src/modules/context-menu/feeds.tsx @@ -15,7 +15,7 @@ import { unreadSyncService } from "@/src/store/unread/store" export const SubscriptionFeedItemContextMenu: FC< PropsWithChildren & { id: string - view: FeedViewType + view?: FeedViewType } > = ({ id, children, view }) => { const allCategories = useListSubscriptionCategory(view) diff --git a/apps/mobile/src/modules/entry-list/action.tsx b/apps/mobile/src/modules/entry-list/action.tsx index 8ccb0f2573..189c47357a 100644 --- a/apps/mobile/src/modules/entry-list/action.tsx +++ b/apps/mobile/src/modules/entry-list/action.tsx @@ -1,51 +1,9 @@ -import { Link } from "expo-router" import { Text, 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 { setIsLoadingArchivedEntries, - useFeedDrawer, useIsLoadingArchivedEntries, -} from "../feed-drawer/atoms" - -const useActionPadding = () => { - const insets = useSafeAreaInsets() - return { paddingLeft: insets.left + 12, paddingRight: insets.right + 12 } -} - -export function HomeLeftAction() { - const { openDrawer } = useFeedDrawer() - - const insets = useActionPadding() - - return ( - - - - ) -} - -export function HomeRightAction() { - const insets = useActionPadding() - - return ( - - - - - - - - ) -} +} from "@/src/modules/screen/atoms" export function LoadArchiveButton() { const isLoadingArchivedEntries = useIsLoadingArchivedEntries() diff --git a/apps/mobile/src/modules/entry-list/entry-list-gird.tsx b/apps/mobile/src/modules/entry-list/entry-list-gird.tsx index c3fe1f6fb0..ca3e62e785 100644 --- a/apps/mobile/src/modules/entry-list/entry-list-gird.tsx +++ b/apps/mobile/src/modules/entry-list/entry-list-gird.tsx @@ -14,11 +14,10 @@ import { useSafeAreaInsets } from "react-native-safe-area-context" import { NavigationContext } from "@/src/components/common/SafeNavigationScrollView" import { ThemedText } from "@/src/components/common/ThemedText" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" +import { useFetchEntriesControls, useSelectedView } from "@/src/modules/screen/atoms" import { useEntry } from "@/src/store/entry/hooks" import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store" -import { useFetchEntriesControls, useSelectedView } from "../feed-drawer/atoms" - export function EntryListContentGrid({ entryIds, ...rest diff --git a/apps/mobile/src/modules/entry-list/entry-list.tsx b/apps/mobile/src/modules/entry-list/entry-list.tsx index a1cfb31cd3..59939b8214 100644 --- a/apps/mobile/src/modules/entry-list/entry-list.tsx +++ b/apps/mobile/src/modules/entry-list/entry-list.tsx @@ -1,140 +1,69 @@ import { FeedViewType } from "@follow/constants" -import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" import type { ListRenderItemInfo } from "@shopify/flash-list" -import { FlashList } from "@shopify/flash-list" import { Image } from "expo-image" import { router } from "expo-router" -import { useCallback, useContext, useMemo } from "react" -import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native" -import { RefreshControl, StyleSheet, Text, useAnimatedValue, View } from "react-native" -import { useSafeAreaInsets } from "react-native-safe-area-context" -import { useColor } from "react-native-uikit-colors" +import { useCallback, useMemo } from "react" +import { StyleSheet, Text, View } from "react-native" -import { - NavigationBlurEffectHeader, - NavigationContext, -} from "@/src/components/common/SafeNavigationScrollView" import { setWebViewEntry } from "@/src/components/native/webview/EntryContentWebView" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" -import { useDefaultHeaderHeight } from "@/src/hooks/useDefaultHeaderHeight" +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, - useSelectedFeedTitle, useSelectedView, -} from "@/src/modules/feed-drawer/atoms" +} from "@/src/modules/screen/atoms" +import { TimelineSelectorHeader } from "@/src/modules/screen/timeline-selector-header" +import { TimelineSelectorList } from "@/src/modules/screen/timeline-selector-list" import { useEntry } from "@/src/store/entry/hooks" import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store" -import { EntryItemContextMenu } from "../context-menu/entry" -import { ViewSelector } from "../feed-drawer/view-selector" -import { HomeLeftAction, HomeRightAction, LoadArchiveButton } from "./action" -import { EntryListContentGrid } from "./entry-list-gird" - -const headerHideableBottomHeight = 58 - export function EntryListScreen({ entryIds }: { entryIds: string[] }) { - const scrollY = useAnimatedValue(0) const view = useSelectedView() - const viewTitle = useSelectedFeedTitle() - const screenType = useEntryListContext().type - const isFeed = screenType === "feed" - const isTimeline = screenType === "timeline" return ( - ({ scrollY }), [scrollY])}> - (isTimeline ? () => : undefined), - [isTimeline], - )} - headerRight={useMemo( - () => (isTimeline ? () => : undefined), - [isTimeline], - )} - headerHideableBottomHeight={isTimeline ? headerHideableBottomHeight : undefined} - headerHideableBottom={isTimeline ? ViewSelector : undefined} - /> + {view === FeedViewType.Pictures || view === FeedViewType.Videos ? ( ) : ( )} - + ) } function EntryListContent({ entryIds }: { entryIds: string[] }) { const screenType = useEntryListContext().type - const insets = useSafeAreaInsets() - - const originalDefaultHeaderHeight = useDefaultHeaderHeight() - const headerHeight = - screenType === "timeline" - ? originalDefaultHeaderHeight + headerHideableBottomHeight - : originalDefaultHeaderHeight - const scrollY = useContext(NavigationContext)?.scrollY - const { fetchNextPage, isFetching, refetch, isRefetching } = useFetchEntriesControls() - const onScroll = useCallback( - (e: NativeSyntheticEvent) => { - scrollY?.setValue(e.nativeEvent.contentOffset.y) - }, - [scrollY], - ) - const renderItem = useCallback( ({ item: id }: ListRenderItemInfo) => , [], ) - const tabBarHeight = useBottomTabBarHeight() - const ListFooterComponent = useMemo( () => isFetching ? : screenType === "feed" ? : null, [isFetching, screenType], ) - const systemFill = useColor("secondaryLabel") - return ( - { - refetch() - }} - refreshing={isRefetching} - /> - } - onScroll={onScroll} + { + refetch() + }} + isRefetching={isRefetching} data={entryIds} renderItem={renderItem} - keyExtractor={(id) => id} onEndReached={() => { fetchNextPage() }} onViewableItemsChanged={({ viewableItems }) => { debouncedFetchEntryContentByStream(viewableItems.map((item) => item.key)) }} - scrollIndicatorInsets={{ - top: headerHeight - insets.top, - bottom: tabBarHeight ? tabBarHeight - insets.bottom : undefined, - }} - estimatedItemSize={100} - contentContainerStyle={{ - paddingTop: headerHeight, - paddingBottom: tabBarHeight, - }} ItemSeparatorComponent={ItemSeparator} ListFooterComponent={ListFooterComponent} /> diff --git a/apps/mobile/src/modules/entry-list/index.tsx b/apps/mobile/src/modules/entry-list/index.tsx index 6d80546702..c4f8a510fa 100644 --- a/apps/mobile/src/modules/entry-list/index.tsx +++ b/apps/mobile/src/modules/entry-list/index.tsx @@ -2,7 +2,7 @@ import type { FeedViewType } from "@follow/constants" import { useIsFocused } from "@react-navigation/native" import { useEffect } from "react" -import { useSelectedFeed, useSetDrawerSwipeDisabled } from "@/src/modules/feed-drawer/atoms" +import { useSelectedFeed, useSetDrawerSwipeDisabled } from "@/src/modules/screen/atoms" import { useEntryIdsByCategory, useEntryIdsByFeedId, diff --git a/apps/mobile/src/modules/screen/action.tsx b/apps/mobile/src/modules/screen/action.tsx new file mode 100644 index 0000000000..5fee97d7a8 --- /dev/null +++ b/apps/mobile/src/modules/screen/action.tsx @@ -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" + +const useActionPadding = () => { + const insets = useSafeAreaInsets() + return { paddingLeft: insets.left + 12, paddingRight: insets.right + 12 } +} + +export function HomeLeftAction() { + const insets = useActionPadding() + + return ( + + + + ) +} + +export function HomeRightAction() { + const insets = useActionPadding() + + return ( + + + + + + + + ) +} diff --git a/apps/mobile/src/modules/screen/atoms.ts b/apps/mobile/src/modules/screen/atoms.ts new file mode 100644 index 0000000000..c4b5af482c --- /dev/null +++ b/apps/mobile/src/modules/screen/atoms.ts @@ -0,0 +1,250 @@ +import { FeedViewType } from "@follow/constants" +import { jotaiStore } from "@follow/utils" +import { atom, useAtom, useAtomValue, useSetAtom } from "jotai" +import { createContext, useCallback, useContext, useMemo } from "react" + +import { views } from "@/src/constants/views" +import { usePrefetchEntries } from "@/src/store/entry/hooks" +import type { FetchEntriesProps } from "@/src/store/entry/types" +import { FEED_COLLECTION_LIST } from "@/src/store/entry/utils" +import { useFeed } from "@/src/store/feed/hooks" +import { useInbox } from "@/src/store/inbox/hooks" +import { useList } from "@/src/store/list/hooks" +import { getSubscriptionByCategory } from "@/src/store/subscription/getter" +import { useSubscription } from "@/src/store/subscription/hooks" + +// drawer open state + +const drawerOpenAtom = atom(false) + +export function useFeedDrawer() { + const [state, setState] = useAtom(drawerOpenAtom) + + return { + isDrawerOpen: state, + openDrawer: useCallback(() => setState(true), [setState]), + closeDrawer: useCallback(() => setState(false), [setState]), + toggleDrawer: useCallback(() => setState(!state), [setState, state]), + } +} + +export const closeDrawer = () => jotaiStore.set(drawerOpenAtom, false) + +// is drawer swipe disabled + +const isDrawerSwipeDisabledAtom = atom(true) + +export function useIsDrawerSwipeDisabled() { + return useAtomValue(isDrawerSwipeDisabledAtom) +} + +export function useSetDrawerSwipeDisabled() { + return useSetAtom(isDrawerSwipeDisabledAtom) +} + +// collection panel selected state + +type CollectionPanelState = + | { + type: "view" + viewId: FeedViewType + } + | { + type: "list" + listId: string + } + +const collectionPanelStateAtom = atom({ + type: "view", + viewId: FeedViewType.Articles, +}) + +export function useSelectedCollection() { + return useAtomValue(collectionPanelStateAtom) +} +export const selectCollection = (state: CollectionPanelState) => { + jotaiStore.set(collectionPanelStateAtom, state) + if (state.type === "view" || state.type === "list") { + jotaiStore.set(selectedTimelineAtom, state) + } +} + +// feed panel selected state + +export type SelectedTimeline = + | { + type: "view" + viewId: FeedViewType + } + | { + type: "list" + listId: string + } + | { + type: "inbox" + inboxId: string + } + +export type SelectedFeed = + | { + type: "feed" + feedId: string + } + | { + type: "category" + categoryName: string + } + | null + +const selectedTimelineAtom = atom({ + type: "view", + viewId: FeedViewType.Articles, +}) + +const selectedFeedAtom = atom(null) + +const isLoadingArchivedEntriesAtom = atom(false) + +export const useIsLoadingArchivedEntries = () => { + return useAtomValue(isLoadingArchivedEntriesAtom) +} +export const setIsLoadingArchivedEntries = (isLoading: boolean) => { + jotaiStore.set(isLoadingArchivedEntriesAtom, isLoading) +} + +export const EntryListContext = createContext<{ type: "timeline" | "feed" }>({ type: "timeline" }) +export const useEntryListContext = () => { + return useContext(EntryListContext) +} + +export function useSelectedView() { + const selectedTimeLine = useAtomValue(selectedTimelineAtom) + const selectedFeed = useAtomValue(selectedFeedAtom) + + const list = useList(selectedTimeLine.type === "list" ? selectedTimeLine.listId : "") + const subscription = useSubscription( + selectedFeed && selectedFeed.type === "feed" ? selectedFeed.feedId : "", + ) + const view = + selectedTimeLine.type === "view" + ? selectedTimeLine.viewId + : selectedTimeLine.type === "list" + ? list?.view + : selectedFeed?.type === "feed" + ? subscription?.view + : undefined + return view +} + +function getFetchEntryPayload( + selectedFeed: SelectedTimeline | SelectedFeed, + isArchived = false, +): FetchEntriesProps | null { + if (!selectedFeed) { + return null + } + + let payload: FetchEntriesProps = {} + switch (selectedFeed.type) { + case "view": { + payload = { view: selectedFeed.viewId } + break + } + case "feed": { + payload = { feedId: selectedFeed.feedId } + break + } + case "category": { + payload = { feedId: getSubscriptionByCategory(selectedFeed.categoryName).join(",") } + break + } + case "list": { + payload = { listId: selectedFeed.listId } + break + } + case "inbox": { + payload = { inboxId: selectedFeed.inboxId } + break + } + // No default + } + + payload.isArchived = isArchived + return payload +} + +export function useSelectedFeed() { + const entryListContext = useEntryListContext() + + const selectedTimeline = useAtomValue(selectedTimelineAtom) + const selectedFeed = useAtomValue(selectedFeedAtom) + + const isArchived = useIsLoadingArchivedEntries() + const payload = getFetchEntryPayload( + entryListContext.type === "feed" ? selectedFeed : selectedTimeline, + entryListContext.type === "feed" ? isArchived : false, + ) + usePrefetchEntries(payload) + + return entryListContext.type === "feed" ? selectedFeed : selectedTimeline +} + +export function useFetchEntriesControls() { + const entryListContext = useEntryListContext() + + const selectedFeed = useSelectedFeed() + const isArchived = useIsLoadingArchivedEntries() + + const payload = getFetchEntryPayload( + selectedFeed, + entryListContext.type === "feed" ? isArchived : false, + ) + return usePrefetchEntries(payload) +} + +export const useSelectedFeedTitle = () => { + const selectedFeed = useSelectedFeed() + + const viewDef = useViewDefinition( + selectedFeed && selectedFeed.type === "view" ? selectedFeed.viewId : undefined, + ) + const feed = useFeed(selectedFeed && selectedFeed.type === "feed" ? selectedFeed.feedId : "") + const list = useList(selectedFeed && selectedFeed.type === "list" ? selectedFeed.listId : "") + const inbox = useInbox(selectedFeed && selectedFeed.type === "inbox" ? selectedFeed.inboxId : "") + + if (!selectedFeed) { + return "" + } + + switch (selectedFeed.type) { + case "view": { + return viewDef?.name + } + case "feed": { + return selectedFeed.feedId === FEED_COLLECTION_LIST ? "Collections" : (feed?.title ?? "") + } + case "category": { + return selectedFeed.categoryName + } + case "list": { + return list?.title + } + case "inbox": { + return inbox?.title ?? "Inbox" + } + } +} + +export const selectTimeline = (state: SelectedTimeline) => { + jotaiStore.set(selectedTimelineAtom, state) +} + +export const selectFeed = (state: SelectedFeed) => { + jotaiStore.set(selectedFeedAtom, state) + jotaiStore.set(isLoadingArchivedEntriesAtom, false) +} + +export const useViewDefinition = (view?: FeedViewType) => { + const viewDef = useMemo(() => views.find((v) => v.view === view), [view]) + return viewDef +} diff --git a/apps/mobile/src/modules/screen/hooks/useHeaderHeight.tsx b/apps/mobile/src/modules/screen/hooks/useHeaderHeight.tsx new file mode 100644 index 0000000000..d7d3ffeb90 --- /dev/null +++ b/apps/mobile/src/modules/screen/hooks/useHeaderHeight.tsx @@ -0,0 +1,16 @@ +import { useDefaultHeaderHeight } from "@/src/hooks/useDefaultHeaderHeight" + +import { useEntryListContext } from "../atoms" + +export const headerHideableBottomHeight = 58 + +export const useHeaderHeight = () => { + const screenType = useEntryListContext().type + const originalDefaultHeaderHeight = useDefaultHeaderHeight() + const headerHeight = + screenType === "timeline" + ? originalDefaultHeaderHeight + headerHideableBottomHeight + : originalDefaultHeaderHeight + + return headerHeight +} diff --git a/apps/mobile/src/modules/screen/timeline-selector-header.tsx b/apps/mobile/src/modules/screen/timeline-selector-header.tsx new file mode 100644 index 0000000000..ff9183c28c --- /dev/null +++ b/apps/mobile/src/modules/screen/timeline-selector-header.tsx @@ -0,0 +1,40 @@ +import { useMemo } from "react" +import { useAnimatedValue } from "react-native" + +import { + NavigationBlurEffectHeader, + NavigationContext, +} from "@/src/components/common/SafeNavigationScrollView" +import { HomeLeftAction, HomeRightAction } from "@/src/modules/screen/action" +import { useEntryListContext, useSelectedFeedTitle } from "@/src/modules/screen/atoms" +import { headerHideableBottomHeight } from "@/src/modules/screen/hooks/useHeaderHeight" +import { ViewSelector } from "@/src/modules/screen/view-selector" + +export function TimelineSelectorHeader({ children }: { children: React.ReactNode }) { + const scrollY = useAnimatedValue(0) + const viewTitle = useSelectedFeedTitle() + const screenType = useEntryListContext().type + + const isFeed = screenType === "feed" + const isTimeline = screenType === "timeline" + return ( + ({ scrollY }), [scrollY])}> + (isTimeline ? () => : undefined), + [isTimeline], + )} + headerRight={useMemo( + () => (isTimeline ? () => : undefined), + [isTimeline], + )} + headerHideableBottomHeight={isTimeline ? headerHideableBottomHeight : undefined} + headerHideableBottom={isTimeline ? ViewSelector : undefined} + /> + {children} + + ) +} diff --git a/apps/mobile/src/modules/screen/timeline-selector-list.tsx b/apps/mobile/src/modules/screen/timeline-selector-list.tsx new file mode 100644 index 0000000000..f3f653ece1 --- /dev/null +++ b/apps/mobile/src/modules/screen/timeline-selector-list.tsx @@ -0,0 +1,67 @@ +import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" +import type { FlashListProps } from "@shopify/flash-list" +import { FlashList } from "@shopify/flash-list" +import { forwardRef, useCallback, useContext } from "react" +import type { NativeScrollEvent, NativeSyntheticEvent } from "react-native" +import { RefreshControl } from "react-native" +import { useSafeAreaInsets } from "react-native-safe-area-context" +import { useColor } from "react-native-uikit-colors" + +import { NavigationContext } from "@/src/components/common/SafeNavigationScrollView" +import { useHeaderHeight } from "@/src/modules/screen/hooks/useHeaderHeight" +import { debouncedFetchEntryContentByStream } from "@/src/store/entry/store" + +type Props = { + onRefresh: () => void + isRefetching: boolean +} + +export const TimelineSelectorList = forwardRef, Props & FlashListProps>( + ({ onRefresh, isRefetching, ...props }, ref) => { + const insets = useSafeAreaInsets() + + const headerHeight = useHeaderHeight() + const scrollY = useContext(NavigationContext)?.scrollY + + const onScroll = useCallback( + (e: NativeSyntheticEvent) => { + scrollY?.setValue(e.nativeEvent.contentOffset.y) + }, + [scrollY], + ) + + const tabBarHeight = useBottomTabBarHeight() + + const systemFill = useColor("secondaryLabel") + + return ( + + } + onScroll={onScroll} + keyExtractor={(id) => id} + onViewableItemsChanged={({ viewableItems }) => { + debouncedFetchEntryContentByStream(viewableItems.map((item) => item.key)) + }} + scrollIndicatorInsets={{ + top: headerHeight - insets.top, + bottom: tabBarHeight ? tabBarHeight - insets.bottom : undefined, + }} + estimatedItemSize={100} + contentContainerStyle={{ + paddingTop: headerHeight, + paddingBottom: tabBarHeight, + }} + {...props} + /> + ) + }, +) diff --git a/apps/mobile/src/modules/feed-drawer/view-selector.tsx b/apps/mobile/src/modules/screen/view-selector.tsx similarity index 98% rename from apps/mobile/src/modules/feed-drawer/view-selector.tsx rename to apps/mobile/src/modules/screen/view-selector.tsx index 41d01ba994..c5343f90c0 100644 --- a/apps/mobile/src/modules/feed-drawer/view-selector.tsx +++ b/apps/mobile/src/modules/screen/view-selector.tsx @@ -9,12 +9,11 @@ import { ReAnimatedTouchableOpacity } from "@/src/components/common/AnimatedComp import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" import type { ViewDefinition } from "@/src/constants/views" import { views } from "@/src/constants/views" +import { selectTimeline, useSelectedFeed } from "@/src/modules/screen/atoms" import { useList } from "@/src/store/list/hooks" import { useAllListSubscription } from "@/src/store/subscription/hooks" import { accentColor, useColor } from "@/src/theme/colors" -import { selectTimeline, useSelectedFeed } from "../feed-drawer/atoms" - export function ViewSelector() { const lists = useAllListSubscription() diff --git a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx index bde8253bae..035365e3a4 100644 --- a/apps/mobile/src/modules/subscription/CategoryGrouped.tsx +++ b/apps/mobile/src/modules/subscription/CategoryGrouped.tsx @@ -5,12 +5,12 @@ import Animated, { useAnimatedStyle, useSharedValue, withSpring } from "react-na import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" import { MingcuteRightLine } from "@/src/icons/mingcute_right_line" +import { closeDrawer, selectFeed, useSelectedFeed } from "@/src/modules/screen/atoms" 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 { GroupedContext } from "./ctx" import { ItemSeparator } from "./ItemSeparator" import { UnGroupedList } from "./UnGroupedList" @@ -33,9 +33,14 @@ export const CategoryGrouped = memo( transform: [{ rotate: `${rotateSharedValue.value}deg` }], } }, [rotateSharedValue]) - const view = useViewPageCurrentView() const tertiaryLabelColor = useColor("tertiaryLabel") + const selectedFeed = useSelectedFeed() + if (selectedFeed?.type !== "view") { + return null + } + const view = selectedFeed.viewId + return ( <> { - const [currentView, setCurrentView] = useAtom(viewAtom) - - const pagerRef = useRef(null) - - useEffect(() => { - pagerRef.current?.setPage(currentView) - }, [currentView]) - - return ( - { - setCurrentView(nativeEvent.position) - }} - scrollEnabled - style={style.flex} - initialPage={0} - ref={pagerRef} - offscreenPageLimit={3} - > - {[ - FeedViewType.Articles, - FeedViewType.SocialMedia, - FeedViewType.Pictures, - FeedViewType.Videos, - FeedViewType.Audios, - FeedViewType.Notifications, - ].map((view) => { - return ( - - - - ) - })} - - ) -}) const keyExtractor = (item: string | { category: string; subscriptionIds: string[] }) => { if (typeof item === "string") { return item } return item.category } -export const SubscriptionList = ({ - view, - additionalOffsetTop, -}: { - view: FeedViewType - additionalOffsetTop?: number -}) => { - const headerHeight = useHeaderHeight() - const insets = useSafeAreaInsets() - const tabHeight = useBottomTabBarHeight() +export const SubscriptionList = ({ view }: { view: FeedViewType }) => { usePrefetchSubscription(view) const { grouped, unGrouped } = useGroupedSubscription(view) @@ -105,38 +46,21 @@ export const SubscriptionList = ({ return subscriptionSyncService.fetch(view) }) - const offsetTop = headerHeight - insets.top + (additionalOffsetTop || 0) - return ( - { - setRefreshing(true) - onRefresh().finally(() => { - setRefreshing(false) - }) - }} - refreshing={refreshing} - /> - } - className={"bg-system-grouped-background"} - contentInsetAdjustmentBehavior="automatic" - scrollIndicatorInsets={{ - bottom: tabHeight - insets.bottom, - top: offsetTop, - }} - contentContainerStyle={{ - paddingTop: offsetTop, - paddingBottom: tabHeight, + { + setRefreshing(true) + onRefresh().finally(() => { + setRefreshing(false) + }) }} + isRefetching={refreshing} ItemSeparatorComponent={ItemSeparator} data={data} ListHeaderComponent={ListHeaderComponent} renderItem={ItemRender} keyExtractor={keyExtractor} - itemLayoutAnimation={LinearTransition} + // itemLayoutAnimation={LinearTransition} extraData={{ total: data.length, }} @@ -169,35 +93,14 @@ const ItemRender = ({ } const ListHeaderComponent = () => { - const view = useViewPageCurrentView() - return ( <> - {view === FeedViewType.Articles && } - Feeds ) } -const InboxList = () => { - const inboxes = useInboxSubscription(FeedViewType.Articles) - if (inboxes.length === 0) return null - return ( - - Inboxes - - - - ) -} -const renderInboxItems = ({ item }: { item: string }) => - const StarItem = () => { return ( { ) } - -const ListList = () => { - const currentView = useViewPageCurrentView() - const listIds = useListSubscription(currentView) - const sortedListIds = useSortedListSubscription(listIds, "alphabet") - if (sortedListIds.length === 0) return null - return ( - - Lists - - - - ) -} -const renderListItems = ({ item }: { item: string }) => - -const style = StyleSheet.create({ - flex: { - flex: 1, - }, -}) diff --git a/apps/mobile/src/modules/subscription/ctx.ts b/apps/mobile/src/modules/subscription/ctx.ts index 7e7cb1a5b7..db33b9ebc1 100644 --- a/apps/mobile/src/modules/subscription/ctx.ts +++ b/apps/mobile/src/modules/subscription/ctx.ts @@ -1,7 +1,7 @@ import type { FeedViewType } from "@follow/constants" -import { createContext, useContext } from "react" +import { createContext } from "react" +// TODO: remove this context const ViewPageCurrentViewContext = createContext(null!) export const ViewPageCurrentViewProvider = ViewPageCurrentViewContext.Provider -export const useViewPageCurrentView = () => useContext(ViewPageCurrentViewContext) export const GroupedContext = createContext(null) diff --git a/apps/mobile/src/modules/subscription/items/InboxItem.tsx b/apps/mobile/src/modules/subscription/items/InboxItem.tsx index 0c986855b6..afbd5022a0 100644 --- a/apps/mobile/src/modules/subscription/items/InboxItem.tsx +++ b/apps/mobile/src/modules/subscription/items/InboxItem.tsx @@ -5,12 +5,11 @@ import Animated, { FadeOutUp } from "react-native-reanimated" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" import { InboxCuteFiIcon } from "@/src/icons/inbox_cute_fi" +import { closeDrawer, selectTimeline } from "@/src/modules/screen/atoms" import { useSubscription } from "@/src/store/subscription/hooks" import { getInboxStoreId } from "@/src/store/subscription/utils" import { useUnreadCount } from "@/src/store/unread/hooks" -import { closeDrawer, selectTimeline } from "../../feed-drawer/atoms" - export const InboxItem = memo(({ id }: { id: string }) => { const subscription = useSubscription(getInboxStoreId(id)) const unreadCount = useUnreadCount(id) diff --git a/apps/mobile/src/modules/subscription/items/ListSubscriptionItem.tsx b/apps/mobile/src/modules/subscription/items/ListSubscriptionItem.tsx index 2062671eb5..374b4eb861 100644 --- a/apps/mobile/src/modules/subscription/items/ListSubscriptionItem.tsx +++ b/apps/mobile/src/modules/subscription/items/ListSubscriptionItem.tsx @@ -4,11 +4,11 @@ import Animated, { FadeOutUp } from "react-native-reanimated" import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" +import { closeDrawer, selectTimeline } from "@/src/modules/screen/atoms" import { useList } from "@/src/store/list/hooks" import { useUnreadCount } from "@/src/store/unread/hooks" import { SubscriptionListItemContextMenu } from "../../context-menu/lists" -import { closeDrawer, selectTimeline } from "../../feed-drawer/atoms" export const ListSubscriptionItem = memo(({ id }: { id: string; className?: string }) => { const list = useList(id) diff --git a/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx b/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx index 77e040718d..9f7d8361c4 100644 --- a/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx +++ b/apps/mobile/src/modules/subscription/items/SubscriptionItem.tsx @@ -6,13 +6,13 @@ import Animated, { FadeOutUp } from "react-native-reanimated" import { FeedIcon } from "@/src/components/ui/icon/feed-icon" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" +import { closeDrawer, selectFeed, useSelectedFeed } from "@/src/modules/screen/atoms" import { useFeed, usePrefetchFeed } from "@/src/store/feed/hooks" import { useSubscription } from "@/src/store/subscription/hooks" import { useUnreadCount } from "@/src/store/unread/hooks" import { SubscriptionFeedItemContextMenu } from "../../context-menu/feeds" -import { closeDrawer, selectFeed } from "../../feed-drawer/atoms" -import { GroupedContext, useViewPageCurrentView } from "../ctx" +import { GroupedContext } from "../ctx" // const renderRightActions = () => { // return ( @@ -48,9 +48,11 @@ export const SubscriptionItem = memo(({ id, className }: { id: string; className const unreadCount = useUnreadCount(id) const feed = useFeed(id)! const inGrouped = !!useContext(GroupedContext) - const view = useViewPageCurrentView() const { isLoading } = usePrefetchFeed(id, { enabled: !subscription && !feed }) + const selectedFeed = useSelectedFeed() + const view = selectedFeed?.type === "view" ? selectedFeed.viewId : undefined + if (isLoading) { return ( diff --git a/apps/mobile/src/screens/(stack)/(tabs)/subscriptions.tsx b/apps/mobile/src/screens/(stack)/(tabs)/subscriptions.tsx index a5e85ec74a..fcc13fa8dd 100644 --- a/apps/mobile/src/screens/(stack)/(tabs)/subscriptions.tsx +++ b/apps/mobile/src/screens/(stack)/(tabs)/subscriptions.tsx @@ -1,3 +1,14 @@ +import { useSelectedFeed } from "@/src/modules/screen/atoms" +import { TimelineSelectorHeader } from "@/src/modules/screen/timeline-selector-header" +import { SubscriptionList } from "@/src/modules/subscription/SubscriptionLists" + export default function Subscriptions() { - return null + const selectedFeed = useSelectedFeed() + const view = selectedFeed?.type === "view" ? selectedFeed.viewId : undefined + + return ( + + + + ) } diff --git a/apps/mobile/src/screens/(stack)/feeds/[feedId]/index.tsx b/apps/mobile/src/screens/(stack)/feeds/[feedId]/index.tsx index b0b2baae55..2e6cd747d3 100644 --- a/apps/mobile/src/screens/(stack)/feeds/[feedId]/index.tsx +++ b/apps/mobile/src/screens/(stack)/feeds/[feedId]/index.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react" import { useSafeAreaInsets } from "react-native-safe-area-context" import { EntryListScreen } from "@/src/modules/entry-list/entry-list" -import { EntryListContext } from "@/src/modules/feed-drawer/atoms" +import { EntryListContext } from "@/src/modules/screen/atoms" import { useEntryIdsByCategory, useEntryIdsByFeedId } from "@/src/store/entry/hooks" export default function Feed() { diff --git a/apps/mobile/src/store/subscription/hooks.ts b/apps/mobile/src/store/subscription/hooks.ts index 5b05fd9377..a064594d5c 100644 --- a/apps/mobile/src/store/subscription/hooks.ts +++ b/apps/mobile/src/store/subscription/hooks.ts @@ -210,11 +210,11 @@ export const useInboxSubscription = (view: FeedViewType) => { ) } -export const useListSubscriptionCategory = (view: FeedViewType) => { +export const useListSubscriptionCategory = (view?: FeedViewType) => { return useSubscriptionStore( useCallback( (state) => { - return Array.from(state.categories[view]) + return view ? Array.from(state.categories[view]) : [] }, [view], ),