import type { FlashListProps, ListRenderItem as FlashListRenderItem, ListRenderItemInfo, } from "@shopify/flash-list"; import * as React from "react"; import { Platform, PressableProps, StyleProp, TextStyle, View, ViewProps, ViewStyle, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Button } from "@/components/ui/Button"; import { Text, TextClassContext } from "@/components/ui/Text"; import { cn } from "@/lib/utils"; import { FlashList } from "@shopify/flash-list"; import { cva } from "class-variance-authority"; import { cssInterop } from "nativewind"; cssInterop(FlashList, { className: "style", contentContainerClassName: "contentContainerStyle", }); type ListDataItem = string | { title: string; subTitle?: string }; type ListVariant = "insets" | "full-width"; type ListRef = React.Ref>; type ListRenderItemProps = ListRenderItemInfo & { variant?: ListVariant; isFirstInSection?: boolean; isLastInSection?: boolean; sectionHeaderAsGap?: boolean; }; type ListProps = Omit< FlashListProps, "renderItem" > & { renderItem?: ListRenderItem; variant?: ListVariant; sectionHeaderAsGap?: boolean; rootClassName?: string; rootStyle?: StyleProp; }; type ListRenderItem = ( props: ListRenderItemProps, ) => ReturnType>; const rootVariants = cva("min-h-2 flex-1", { variants: { variant: { insets: "ios:px-4", "full-width": "ios:bg-card ios:dark:bg-background", }, sectionHeaderAsGap: { true: "", false: "", }, }, compoundVariants: [ { variant: "full-width", sectionHeaderAsGap: true, className: "bg-card dark:bg-background", }, ], defaultVariants: { variant: "full-width", sectionHeaderAsGap: false, }, }); function ListComponent({ variant = "full-width", rootClassName, rootStyle, contentContainerClassName, renderItem, data, sectionHeaderAsGap = false, contentInsetAdjustmentBehavior = "automatic", ...props }: ListProps) { const insets = useSafeAreaInsets(); return ( ); } function getItemType(item: T) { return typeof item === "string" ? "sectioHeader" : "row"; } function renderItemWithVariant( renderItem: ListRenderItem | null | undefined, variant: ListVariant, data: readonly T[] | null | undefined, sectionHeaderAsGap?: boolean, ) { return (args: ListRenderItemProps) => { const previousItem = data?.[args.index - 1]; const nextItem = data?.[args.index + 1]; return renderItem ? renderItem({ ...args, variant, isFirstInSection: !previousItem || typeof previousItem === "string", isLastInSection: !nextItem || typeof nextItem === "string", sectionHeaderAsGap, }) : null; }; } const List = React.forwardRef(ListComponent) as ( props: ListProps & { ref?: ListRef }, ) => React.ReactElement; function isPressable(props: PressableProps) { return ( ("onPress" in props && props.onPress) || ("onLongPress" in props && props.onLongPress) || ("onPressIn" in props && props.onPressIn) || ("onPressOut" in props && props.onPressOut) || ("onLongPress" in props && props.onLongPress) ); } type ListItemProps = PressableProps & ListRenderItemProps & { androidRootClassName?: string; titleClassName?: string; titleStyle?: StyleProp; textNumberOfLines?: number; subTitleClassName?: string; subTitleStyle?: StyleProp; subTitleNumberOfLines?: number; textContentClassName?: string; leftView?: React.ReactNode; rightView?: React.ReactNode; removeSeparator?: boolean; }; type ListItemRef = React.Ref; const itemVariants = cva("ios:gap-0 flex-row gap-0 bg-card", { variants: { variant: { insets: "ios:bg-card bg-card/70", "full-width": "bg-card dark:bg-background", }, sectionHeaderAsGap: { true: "", false: "", }, isFirstItem: { true: "", false: "", }, isFirstInSection: { true: "", false: "", }, removeSeparator: { true: "", false: "", }, isLastInSection: { true: "", false: "", }, disabled: { true: "opacity-70", false: "opacity-100", }, }, compoundVariants: [ { variant: "insets", sectionHeaderAsGap: true, className: "ios:dark:bg-card dark:bg-card/70", }, { variant: "insets", isFirstInSection: true, className: "ios:rounded-t-[10px]", }, { variant: "insets", isLastInSection: true, className: "ios:rounded-b-[10px]", }, { removeSeparator: false, isLastInSection: true, className: "ios:border-b-0 border-b border-border/25 dark:border-border/80", }, { variant: "insets", isFirstItem: true, className: "border-t border-border/40", }, ], defaultVariants: { variant: "insets", sectionHeaderAsGap: false, isFirstInSection: false, isLastInSection: false, disabled: false, }, }); function ListItemComponent( { item, isFirstInSection, isLastInSection, index: _index, variant, className, androidRootClassName, titleClassName, titleStyle, textNumberOfLines, subTitleStyle, subTitleClassName, subTitleNumberOfLines, textContentClassName, sectionHeaderAsGap, removeSeparator = false, leftView, rightView, disabled, ...props }: ListItemProps, ref: ListItemRef, ) { if (typeof item === "string") { console.log( "List.tsx", "ListItemComponent", "Invalid item of type 'string' was provided. Use ListSectionHeader instead.", ); return null; } return ( <> {!removeSeparator && Platform.OS !== "ios" && !isLastInSection && ( )} ); } const ListItem = React.forwardRef(ListItemComponent) as < T extends ListDataItem, >( props: ListItemProps & { ref?: ListItemRef }, ) => React.ReactElement; type ListSectionHeaderProps = ViewProps & ListRenderItemProps & { textClassName?: string; }; type ListSectionHeaderRef = React.Ref; function ListSectionHeaderComponent( { item, isFirstInSection: _isFirstInSection, isLastInSection: _isLastInSection, index: _index, variant, className, textClassName, sectionHeaderAsGap, ...props }: ListSectionHeaderProps, ref: ListSectionHeaderRef, ) { if (typeof item !== "string") { console.log( "List.tsx", "ListSectionHeaderComponent", "Invalid item provided. Expected type 'string'. Use ListItem instead.", ); return null; } if (sectionHeaderAsGap) { return ( ); } return ( {item} ); } const ListSectionHeader = React.forwardRef(ListSectionHeaderComponent) as < T extends ListDataItem, >( props: ListSectionHeaderProps & { ref?: ListSectionHeaderRef }, ) => React.ReactElement; const ESTIMATED_ITEM_HEIGHT = { titleOnly: Platform.select({ ios: 45, default: 57 }), withSubTitle: 56, }; function getStickyHeaderIndices(data: T[]) { if (!data) return []; const indices: number[] = []; for (let i = 0; i < data.length; i++) { if (typeof data[i] === "string") { indices.push(i); } } return indices; } export { ESTIMATED_ITEM_HEIGHT, List, ListItem, ListSectionHeader, getStickyHeaderIndices, }; export type { ListDataItem, ListItemProps, ListProps, ListRenderItemInfo, ListSectionHeaderProps, };