Folo/apps/mobile/src/modules/entry-list/EntryListContentPicture.tsx

142 lines
5.0 KiB
TypeScript

import { isFreeRole } from "@follow/constants"
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { usePrefetchEntryTranslation } from "@follow/store/translation/hooks"
import { useUserRole } from "@follow/store/user/hooks"
import type { FlashListProps, FlashListRef } from "@shopify/flash-list"
import type { ElementRef } from "react"
import { useImperativeHandle, useRef } from "react"
import { StyleSheet, View } from "react-native"
import { useActionLanguage, useGeneralSettingKey } from "@/src/atoms/settings/general"
import { useBottomTabBarHeight } from "@/src/components/layouts/tabbar/hooks"
import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator"
import { useEntries } from "@/src/modules/screen/atoms"
import { useHeaderHeight } from "@/src/modules/screen/hooks/useHeaderHeight"
import { TimelineSelectorMasonryList } from "../screen/TimelineSelectorList"
import { GridEntryListFooter } from "./EntryListFooter"
import { useOnViewableItemsChanged } from "./hooks"
// import type { MasonryItem } from "./templates/EntryGridItem"
import { EntryPictureItem } from "./templates/EntryPictureItem"
export const EntryListContentPicture = ({
ref: forwardRef,
entryIds,
active,
...rest
}: { entryIds: string[] | null; active?: boolean } & Omit<
FlashListProps<string>,
"data" | "renderItem"
> & { ref?: React.Ref<ElementRef<typeof TimelineSelectorMasonryList> | null> }) => {
const ref = useRef<FlashListRef<any>>(null)
useImperativeHandle(forwardRef, () => ref.current!)
const { fetchNextPage, refetch, isRefetching, hasNextPage, isFetching, isReady } = useEntries()
const { onViewableItemsChanged, onScroll, viewableItems } = useOnViewableItemsChanged({
disabled: active === false || isFetching,
})
const translation = useGeneralSettingKey("translation")
const actionLanguage = useActionLanguage()
const userRole = useUserRole()
const translationPrefetchEnabled = translation && !isFreeRole(userRole)
usePrefetchEntryTranslation({
entryIds: active ? viewableItems.map((item) => item.key) : [],
language: actionLanguage,
enabled: translationPrefetchEnabled,
})
const renderItem = useTypeScriptHappyCallback(({ item }: { item: string }) => {
return <EntryPictureItem id={item} />
}, [])
const headerHeight = useHeaderHeight()
const tabBarHeight = useBottomTabBarHeight()
// Show loading skeleton when entries are not ready and no data yet
if (!isReady && (!entryIds || entryIds.length === 0)) {
return (
<View
className="flex-1 px-2"
style={{ paddingTop: headerHeight, paddingBottom: tabBarHeight }}
>
<View className="flex-row">
<View className="mr-1 flex-1">
{Array.from({ length: 4 }).map((_, index) => (
<EntryPictureItemSkeleton key={`left-${index}`} />
))}
</View>
<View className="ml-1 flex-1">
{Array.from({ length: 4 }).map((_, index) => (
<EntryPictureItemSkeleton key={`right-${index}`} />
))}
</View>
</View>
</View>
)
}
return (
<TimelineSelectorMasonryList
ref={ref}
isRefetching={isRefetching}
data={entryIds}
renderItem={renderItem}
keyExtractor={defaultKeyExtractor}
onViewableItemsChanged={onViewableItemsChanged}
onScroll={onScroll}
onEndReached={fetchNextPage}
numColumns={2}
contentContainerStyle={styles.contentContainer}
ListFooterComponent={
hasNextPage ? (
<View className="h-20 items-center justify-center">
<PlatformActivityIndicator />
</View>
) : (
<GridEntryListFooter />
)
}
{...rest}
onRefresh={refetch}
/>
)
}
function EntryPictureItemSkeleton() {
const heights = [120, 160, 140, 180, 100, 200] // Different heights for variety
const randomHeight = heights[Math.floor(Math.random() * heights.length)]
return (
<View className="mx-1 mb-2">
{/* Image placeholder */}
<View className="animate-pulse rounded-md bg-system-fill" style={{ height: randomHeight }} />
{/* Footer content */}
<View className="gap-2 px-1 py-2">
{/* Title lines */}
<View className="gap-1">
<View className="h-3 animate-pulse rounded-md bg-system-fill" style={{ width: "90%" }} />
<View className="h-3 animate-pulse rounded-md bg-system-fill" style={{ width: "70%" }} />
</View>
{/* Feed info */}
<View className="flex-row items-center gap-1.5">
<View className="size-3.5 animate-pulse rounded-full bg-system-fill" />
<View className="h-3 animate-pulse rounded-md bg-system-fill" style={{ width: "40%" }} />
<View className="h-3 animate-pulse rounded-md bg-system-fill" style={{ width: "30%" }} />
</View>
</View>
</View>
)
}
const styles = StyleSheet.create({
contentContainer: {
paddingHorizontal: 8,
},
})
const defaultKeyExtractor = (item: string) => {
return item
}