Folo/apps/mobile/src/modules/subscription/SubscriptionLists.tsx

257 lines
8.3 KiB
TypeScript

import type { FeedViewType } from "@follow/constants"
import type { FlashList } from "@shopify/flash-list"
import type { ParseKeys } from "i18next"
import { memo, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { Text, View } from "react-native"
import { useEventCallback } from "usehooks-ts"
import { useRegisterNavigationScrollView } from "@/src/components/layouts/tabbar/hooks"
import {
GROUPED_ICON_TEXT_GAP,
GROUPED_LIST_ITEM_PADDING,
GROUPED_LIST_MARGIN,
GROUPED_SECTION_BOTTOM_MARGIN,
GROUPED_SECTION_TOP_MARGIN,
} from "@/src/components/ui/grouped/constants"
import { GroupedInsetListCard } from "@/src/components/ui/grouped/GroupedList"
import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { StarCuteFiIcon } from "@/src/icons/star_cute_fi"
import { useNavigation } from "@/src/lib/navigation/hooks"
import { selectFeed } from "@/src/modules/screen/atoms"
import { TimelineSelectorList } from "@/src/modules/screen/TimelineSelectorList"
import { FeedScreen } from "@/src/screens/(stack)/feeds/[feedId]/FeedScreen"
import { FEED_COLLECTION_LIST } from "@/src/store/entry/utils"
import {
useGroupedSubscription,
useInboxSubscription,
useListSubscription,
useSortedGroupedSubscription,
useSortedListSubscription,
useSortedUngroupedSubscription,
} from "@/src/store/subscription/hooks"
import { subscriptionSyncService } from "@/src/store/subscription/store"
import { usePagerListPerformanceHack } from "../entry-list/hooks"
import { useFeedListSortMethod, useFeedListSortOrder } from "./atoms"
import { CategoryGrouped } from "./CategoryGrouped"
import { InboxItem } from "./items/InboxItem"
import { ListSubscriptionItem } from "./items/ListSubscriptionItem"
import { SubscriptionItem } from "./items/SubscriptionItem"
const keyExtractor = (item: string | { category: string; subscriptionIds: string[] }) => {
if (typeof item === "string") {
return item
}
return item.category
}
const SubscriptionListImpl = ({
view,
active = true,
}: {
view: FeedViewType
active?: boolean
}) => {
const listIds = useListSubscription(view)
const sortedListIds = useSortedListSubscription(listIds, "alphabet")
const inboxes = useInboxSubscription(view)
const { grouped, unGrouped } = useGroupedSubscription(view)
const sortBy = useFeedListSortMethod()
const sortOrder = useFeedListSortOrder()
const sortedGrouped = useSortedGroupedSubscription(view, grouped, sortBy, sortOrder)
const sortedUnGrouped = useSortedUngroupedSubscription(unGrouped, sortBy, sortOrder)
const data = useMemo(
() => [
"words.starred",
"words.lists",
...sortedListIds,
"words.inbox",
...inboxes,
"words.feeds",
...sortedGrouped,
...sortedUnGrouped,
],
[inboxes, sortedListIds, sortedGrouped, sortedUnGrouped],
)
const extraData = useMemo(() => {
const listsIndexStart = 2
const listsIndexEnd = listsIndexStart + sortedListIds.length - 1
const inboxIndexStart = listsIndexEnd + 2
const inboxIndexEnd = inboxIndexStart + inboxes.length - 1
const feedsIndexStart = inboxIndexEnd + 2
const feedsIndexEnd = feedsIndexStart + sortedGrouped.length + sortedUnGrouped.length - 1
const groupedIndexStart = inboxIndexEnd + 2
const groupedIndexEnd = groupedIndexStart + sortedGrouped.length - 1
return {
inboxIndexRange: [inboxIndexStart, inboxIndexEnd],
feedsIndexRange: [feedsIndexStart, feedsIndexEnd],
listsIndexRange: [listsIndexStart, listsIndexEnd],
groupedIndexRange: [groupedIndexStart, groupedIndexEnd],
}
}, [inboxes.length, sortedGrouped.length, sortedListIds.length, sortedUnGrouped.length])
const [refreshing, setRefreshing] = useState(false)
const onRefresh = useEventCallback(() => {
return subscriptionSyncService.fetch(view)
})
const scrollViewRef = useRegisterNavigationScrollView<FlashList<any> | null>(active)
const { onScroll, style } = usePagerListPerformanceHack(scrollViewRef)
return (
<TimelineSelectorList
contentContainerClassName="pb-6"
ref={scrollViewRef}
onRefresh={() => {
setRefreshing(true)
onRefresh().finally(() => {
setRefreshing(false)
})
}}
isRefetching={refreshing}
data={data}
estimatedItemSize={50}
renderItem={ItemRender}
keyExtractor={keyExtractor}
extraData={extraData}
onScroll={onScroll}
style={style}
/>
)
}
const ItemRender = ({
item,
index,
extraData,
}: {
item: string | { category: string; subscriptionIds: string[] }
index: number
extraData?: {
inboxIndexRange: [number, number]
feedsIndexRange: [number, number]
listsIndexRange: [number, number]
groupedIndexRange: [number, number]
}
}) => {
if (typeof item === "string") {
switch (item) {
case "words.starred": {
return <StarItem />
}
case "words.inbox": {
if (!extraData) return null
const { inboxIndexRange } = extraData
if (inboxIndexRange[0] > inboxIndexRange[1]) return null
return <SectionTitle transKey={item} />
}
case "words.lists": {
if (!extraData) return null
const { listsIndexRange } = extraData
if (listsIndexRange[0] > listsIndexRange[1]) return null
return <SectionTitle transKey={item} />
}
case "words.feeds": {
if (!extraData) return null
const { feedsIndexRange } = extraData
if (feedsIndexRange[0] > feedsIndexRange[1]) return null
return <SectionTitle transKey={item} />
}
default: {
if (!extraData) return null
const { inboxIndexRange, feedsIndexRange, listsIndexRange } = extraData
if (listsIndexRange[0] <= index && index <= listsIndexRange[1]) {
const isFirst = index === listsIndexRange[0]
const isLast = index === listsIndexRange[1]
return <ListSubscriptionItem id={item} isFirst={isFirst} isLast={isLast} />
}
if (inboxIndexRange[0] <= index && index <= inboxIndexRange[1]) {
const isFirst = index === inboxIndexRange[0]
const isLast = index === inboxIndexRange[1]
return <InboxItem id={item} isFirst={isFirst} isLast={isLast} />
}
if (feedsIndexRange[0] <= index && index <= feedsIndexRange[1]) {
const isFirst = index === feedsIndexRange[0]
const isLast = index === feedsIndexRange[1]
return <SubscriptionItem id={item} isFirst={isFirst} isLast={isLast} />
}
return null
}
}
}
const { category, subscriptionIds } = item
if (!extraData) return null
const { feedsIndexRange } = extraData
const isFirst = index === feedsIndexRange[0]
const isLast = index === feedsIndexRange[1]
return (
<CategoryGrouped
category={category}
subscriptionIds={subscriptionIds}
isFirst={isFirst}
isLast={isLast}
/>
)
}
const SectionTitle = ({ transKey }: { transKey: ParseKeys<"common"> }) => {
const { t } = useTranslation("common")
return (
<View
style={{
marginHorizontal: GROUPED_LIST_MARGIN,
marginTop: GROUPED_SECTION_TOP_MARGIN,
marginBottom: GROUPED_SECTION_BOTTOM_MARGIN,
paddingHorizontal: GROUPED_LIST_ITEM_PADDING,
}}
>
<Text className="text-secondary-label" ellipsizeMode="tail" numberOfLines={1}>
{t(transKey)}
</Text>
</View>
)
}
const StarItem = () => {
const navigation = useNavigation()
const { t } = useTranslation("common")
return (
<GroupedInsetListCard showSeparator={false} className="mt-4">
<ItemPressable
itemStyle={ItemPressableStyle.Grouped}
onPress={() => {
selectFeed({ type: "feed", feedId: FEED_COLLECTION_LIST })
navigation.pushControllerView(FeedScreen, {
feedId: FEED_COLLECTION_LIST,
})
}}
className="h-12 w-full flex-row items-center px-3"
>
<StarCuteFiIcon color="rgb(245, 158, 11)" height={20} width={20} />
<Text className="text-text ml-2 font-medium" style={{ marginLeft: GROUPED_ICON_TEXT_GAP }}>
{t("words.starred")}
</Text>
</ItemPressable>
</GroupedInsetListCard>
)
}
export const SubscriptionList = memo(SubscriptionListImpl)