import React, { memo, useCallback, useDeferredValue, useEffect, useMemo, useRef, } from "react"; import { ActivityIndicator, ScrollView, StyleSheet, Text, View, } from "react-native"; import { Button } from "./Button"; import { ItemIconProps } from "./Button/ItemIcon"; import { ItemProps } from "./Button/Item"; import { ItemLabelProps } from "./Button/ItemLabel"; import { getItemIconProps, getItemLabelProps, getItemProps } from "./utils"; import { useTheme } from "@applicaster/zapp-react-native-utils/theme"; import { ContentViewModel, OPERATIONS, hasOperation, useBottomSheetContent, } from "@applicaster/zapp-react-native-utils/modalState"; import { getCell } from "./presets"; import type { PluginConfiguration } from "./"; import { ModalHeader as DefaultModalHeader } from "./Header"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { SortableList } from "./SortableList"; import { AudioPlayerButton, NowPlayingHeaderSection, VARIANT_WITH_ICON, } from "./AudioPlayer/Components"; import { BottomSheetDragArea } from "./BottomSheetDragArea"; import { ModalBlocksStyleContext } from "./ModalBlocksStyleContext"; import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks"; import { ModalBlocksConfig, parseModalBlocksConfiguration, } from "@applicaster/zapp-react-native-utils/configurationUtils/modalBlocksParser"; import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils"; const styles = StyleSheet.create({ centered: { justifyContent: "center", alignItems: "center", padding: 16, }, errorText: { color: "#ff453a", fontSize: 14, textAlign: "center", }, scrollView: { flexShrink: 1, }, someStyles: { paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 }, }); type ModalComponentProps = { items: any[]; current_selection?: any; onPress: (item: any) => void; width: number; currentRoute: boolean; summary?: string; title?: string; maxHeight?: number; dismiss: () => void; headerComponent?: React.ComponentType; buttonComponent?: React.ComponentType; iconProps?: ItemIconProps | ((theme: PluginConfiguration) => ItemIconProps); itemProps?: | ItemProps | ((theme: PluginConfiguration, width: number) => ItemProps); labelProps?: | ItemLabelProps | ((theme: PluginConfiguration, width: number) => ItemLabelProps); getSelectedItemIcon?: ( theme: PluginConfiguration ) => ItemIconProps["asset"] | null; getDefaultItemIcon?: ( theme: PluginConfiguration ) => ItemIconProps["asset"] | null; iconPlacement?: "left" | "right"; viewModel?: ContentViewModel; /** Optional body that replaces the items list (header stays DefaultModalHeader). */ contentComponent?: React.ComponentType; contentComponentProps?: Record; styleOverrides?: ModalBlocksConfig | Record; measurementPhase?: boolean; }; function _BottomSheetModalContent(props: ModalComponentProps) { const { items, currentRoute, current_selection = null, onPress, dismiss, summary, title, headerComponent, viewModel = null, buttonComponent: ButtonComponent = Button, getSelectedItemIcon = ({ modal_bottom_sheet_item_selected_icon, }: BaseThemePropertiesMobile) => modal_bottom_sheet_item_selected_icon, getDefaultItemIcon = () => null, iconPlacement, contentComponent: ContentComponent, contentComponentProps = {}, styleOverrides: propStyleOverrides, measurementPhase = false, } = props; const route = useRef(currentRoute); const theme = useTheme(); const bottomInset = useSafeAreaInsets().bottom; const paddingTop = Number(theme.modal_bottom_sheet_padding_top); const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom); const calculatedcontentContainerStyle = useMemo(() => { return { paddingBottom: paddingBottom + bottomInset, }; }, [paddingBottom, bottomInset]); const { items: fetchedItems, showCentralLoading, error, handleItemPress, role, behavior, editableCollection, themePluginId, } = useBottomSheetContent({ onPress, viewModel, }); const plugins = usePlugins(); const plugin = useMemo( () => plugins?.find((p) => p.identifier === themePluginId), [plugins, themePluginId] ); const styleOverrides = useMemo(() => { if (propStyleOverrides) { return propStyleOverrides; } const configuration = plugin?.configuration || (plugin as any)?.configuration_json; if (!configuration || !themePluginId) return null; return parseModalBlocksConfiguration(configuration, themePluginId); }, [propStyleOverrides, plugin, themePluginId]); const pluginLocalizations = useLocalizedStrings({ localizations: plugin?.configuration?.localizations || (plugin as any)?.configuration_json?.localizations, }); const normalizedPluginId = themePluginId?.replace(/-/g, "_"); const localizedHeaderTitle = normalizedPluginId ? pluginLocalizations?.[`${normalizedPluginId}_header_title`] : undefined; const createPlaylistTitle = pluginLocalizations?.create_new_playlist_title || "Create New Playlist"; const nextUpTitle = pluginLocalizations?.next_up_title || (normalizedPluginId ? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`] : undefined) || "Next Up"; const nowPlayingTitle = pluginLocalizations?.now_playing_title || (normalizedPluginId ? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`] : undefined) || "Now Playing"; const resolvedTitle = props.title || localizedHeaderTitle || title; const itemsToRender = viewModel ? fetchedItems : items; const operations = editableCollection?.operations || []; const isSortable = useDeferredValue( !measurementPhase && hasOperation(operations, OPERATIONS.REORDER) ); const cell = getCell( viewModel ? role : undefined, viewModel ? behavior : undefined ); const Header = headerComponent ?? DefaultModalHeader; useEffect(() => { if (currentRoute !== route.current) { props.dismiss(); } }, [currentRoute]); const iconBaseProps = useMemo(() => { return getItemIconProps(theme, props.iconProps); }, [theme, props.iconProps]); const itemBaseProps = useMemo(() => { return getItemProps( theme, props.width, props.itemProps, styleOverrides?.item ); }, [theme, props.width, props.itemProps, styleOverrides?.item]); const labelBaseProps = useMemo(() => { return getItemLabelProps( theme, props.width, props.labelProps, styleOverrides?.item ); }, [theme, props.width, props.labelProps, styleOverrides?.item]); const handleStaticPress = useCallback( (item: any) => { onPress(item); dismiss(); }, [onPress, dismiss] ); const onItemPress = viewModel ? handleItemPress : handleStaticPress; const handleDragEnd = useCallback( (params: any) => { const from = params?.fromIndex ?? params?.from; const to = params?.toIndex ?? params?.to; const reorderedItemIds = Array.isArray(params?.data) ? params.data .filter((item: any) => !item?.isHeader) .map((item: any) => item?.id) .filter(Boolean) : undefined; if ( (reorderedItemIds?.length || (typeof from === "number" && typeof to === "number" && from !== to)) && editableCollection?.move ) { editableCollection.move(reorderedItemIds || []); } }, [editableCollection] ); const renderContext = useMemo( () => ({ theme, ButtonComponent, current_selection, iconBaseProps, itemBaseProps, labelBaseProps, getSelectedItemIcon, getDefaultItemIcon, iconPlacement, editableCollection, behavior, }), [ theme, ButtonComponent, current_selection, iconBaseProps, itemBaseProps, labelBaseProps, getSelectedItemIcon, getDefaultItemIcon, iconPlacement, editableCollection, behavior, ] ); const sortableListRenderItem = useCallback( ({ item, index, renderHandle }) => cell.renderItem({ item, index, onPress: onItemPress, context: renderContext, renderHandle, }), [cell, onItemPress, renderContext] ); const hasNowPlaying = hasOperation(operations, OPERATIONS.NOW_PLAYING); const hasClearAll = hasOperation(operations, OPERATIONS.CLEAR_ALL); const hasAdd = hasOperation(operations, OPERATIONS.ADD); const handleDeleteCollection = useCallback(() => { editableCollection?.deleteCollection?.(); }, [editableCollection]); const handleCreatePlaylist = useCallback(() => { editableCollection?.add?.({ id: "create_new_playlist", title: createPlaylistTitle, }); }, [editableCollection, createPlaylistTitle]); const headers = useMemo(() => { const list: Array<{ index: number; component: () => React.ReactElement }> = []; if (hasNowPlaying) { list.push({ index: list.length, component: () => ( ), }); } if (hasAdd) { list.push({ index: list.length, component: () => ( ), }); } return list.length > 0 ? list : undefined; }, [ hasNowPlaying, hasClearAll, handleDeleteCollection, nextUpTitle, nowPlayingTitle, hasAdd, handleCreatePlaylist, createPlaylistTitle, ]); const _items = useMemo(() => { return (itemsToRender || []).map((item, index) => cell.renderItem({ item, index, onPress: onItemPress, context: renderContext, }) ); }, [itemsToRender, cell, onItemPress, renderContext]); const _styles = useMemo(() => { return { maxHeight: props.maxHeight, paddingTop: paddingTop, }; }, [props.maxHeight, paddingTop]); const renderedBody = (() => { if (ContentComponent) { const shouldWrapInScrollView = !contentComponentProps?.disableScrollViewWrap && !(ContentComponent as any)?.disableScrollViewWrap; if (shouldWrapInScrollView) { return ( ); } return ( ); } if (showCentralLoading) { const loaderColor = (theme as any)?.modal_bottom_sheet_activity_indicator_color || (theme as any)?.cell_title_color || "#ffffff"; return ( ); } if (error) { return ( {typeof error === "string" ? error : error.message} ); } if (isSortable) { return ( ); } return ( {hasNowPlaying ? ( ) : null} {hasAdd ? ( ) : null} {_items} ); })(); return (
{renderedBody} ); } export const BottomSheetModalContent = memo(_BottomSheetModalContent);