import { ReactElement, useMemo, useCallback, useState } from 'react' import { getNodeY, PushTreeState, getNode } from 'ducks/pushTreeMap' import { GROUP, LIST, SECTION, responsivePositioningOptions, } from '@adalo/constants' import { useSelector } from 'react-redux' import { hasNotch, hasDynamicIsland } from 'react-native-device-info' import { LayoutChangeEvent, Platform, StatusBar } from 'react-native' import { getResponsiveObject, getRenderObject, getPositioningLayout, getScreenSize, } from 'utils/responsive' import { isPWA } from 'utils/analytics' import { RunnerObject, Layout, ComponentFixedPosition, DeviceType, LayoutMetadata, } from 'types' import { IApp } from 'interfaces' import { getDeviceType } from 'utils/device' const { FIXED_ON_SCROLL } = responsivePositioningOptions export const getLayoutCacheKey = (previewType?: string): string => { const { width } = getScreenSize() const deviceType = getDeviceType(width) const formattedPreviewType = typeof previewType === 'string' && previewType.length > 0 ? previewType : 'default' return `${formattedPreviewType}-${deviceType}` } export const useResponsiveObject = ( object: RunnerObject, newY: number, parentLayoutMetadata: LayoutMetadata, app?: IApp, layoutCacheKey?: string ): { renderObject: RunnerObject positioningLayout: Layout layoutMetadata: LayoutMetadata } => { const { width } = getScreenSize() const deviceObject = useMemo( () => getResponsiveObject(object, width, app), [object, width, layoutCacheKey] ) const renderObject = useMemo( () => getRenderObject(deviceObject), [deviceObject, layoutCacheKey] ) const { layout: positioningLayout, layoutMetadata } = getPositioningLayout( deviceObject, newY, parentLayoutMetadata ) return { renderObject, positioningLayout, layoutMetadata } } type State = { pushTreeMap: PushTreeState } export const usePushNode = ( componentId: string, pushId: string, isFixed: boolean ): { newY: number; nodeHeight: number } => { const newY = useSelector(state => getNodeY(state as State, componentId, pushId) ) const node = useSelector(state => getNode(state as State, componentId, pushId) ) let nodeHeight = node?.height || 0 if (isFixed) { // fixed objects with a list inside need to have their height set to the original height if (node?.pushNode?.children?.some(c => c.type === LIST)) { nodeHeight = node?.pushNode?.originalHeight || nodeHeight } return { nodeHeight, newY: 0 } } return { newY, nodeHeight } } export type HandleLayoutChangeFunction = (e: LayoutChangeEvent) => void export interface HandleLayoutParameters { renderObject: RunnerObject setHeight: (pushId: string, obj: RunnerObject, height: number) => void nodeHeight: number pushId: string layoutCacheKey: string } export const useHandleLayoutChange = ({ renderObject, setHeight, nodeHeight, pushId, layoutCacheKey, }: HandleLayoutParameters): HandleLayoutChangeFunction => { const handleLayoutChange = useCallback( (event: LayoutChangeEvent): void => { const { height: newHeight } = event.nativeEvent.layout if ( renderObject.type !== GROUP && renderObject.type !== LIST && renderObject.responsivity?.verticalPositioning !== FIXED_ON_SCROLL ) { setHeight(pushId, renderObject, newHeight) } }, [renderObject, setHeight, nodeHeight, pushId, layoutCacheKey] ) return handleLayoutChange } export const useRenderedChildren = ( renderObject: RunnerObject, renderChildren: ( children: RunnerObject[], item: any, parentLayoutMetadata: LayoutMetadata ) => ReactElement[], visible: boolean, active: boolean, layoutMetadata: LayoutMetadata, layoutCacheKey: string ): null | ReactElement[] => { const { children } = renderObject const isResponsiveList = renderObject.type === LIST const renderedChildren = useMemo( () => visible && children && !isResponsiveList ? renderChildren(children, undefined, layoutMetadata) : null, [children, visible, active, layoutMetadata, layoutCacheKey] ) return renderedChildren } export const useVerticallyExpandedFixedComponent = ( position: ComponentFixedPosition, positioningLayout: Layout, renderObject: RunnerObject, previewType: string, layoutCacheKey: string, deviceType: DeviceType ) => { const [expandedTopFixedComponent, setExpandedTopFixedComponent] = useState('') const [expandedBottomFixedComponent, setExpandedBottomFixedComponent] = useState('') // prettier-ignore let statusBarHeight if (StatusBar.currentHeight) { statusBarHeight = StatusBar.currentHeight } // If we are in the previewer we set a fixed margin for the status br const WEB_STATUSBAR_OFFSET = previewType === 'mobile' ? 64 : 0 // TODO(dyego) we might be able to simplify a bit more this logic around statusBarHeight // iOS does not provide a value for StatusBar.currentHeight // so we need to infer it based on the device characteristics (notch and dynamic island) if (!statusBarHeight || statusBarHeight === 0) { statusBarHeight = (hasNotch() && hasDynamicIsland()) || Platform.OS === 'web' ? WEB_STATUSBAR_OFFSET : 44 } // for web, we don't have the StatusBar.currentHeight value, so we set to a static offset // expected for the previewer statusBarHeight = Platform.OS !== 'web' && !isPWA() ? statusBarHeight : WEB_STATUSBAR_OFFSET let safeAreaTop = 0 if (isPWA()) { const rootStyle = getComputedStyle(document.documentElement) const insetArea = rootStyle .getPropertyValue('--top-inset-area') .trim() .replace('px', '') safeAreaTop = Number(insetArea) || 0 } // for PWAs, we take the inset areas provided by the css env variables // and apply them to the statusBarHeight. const PWA_TOP_OFFSET = safeAreaTop statusBarHeight = Platform.OS === 'web' && isPWA() ? PWA_TOP_OFFSET : statusBarHeight let newBottom = positioningLayout.bottom || 0 let newTop = positioningLayout.top || 0 const childRectangle = renderObject.children?.find( child => child.type === SECTION && (child.layout.top === 0 || child.layout.bottom === 0) ) let newHeight = renderObject.type === SECTION ? renderObject.layout.height || 0 : childRectangle?.attributes.height || 0 const { borderWidth } = renderObject.attributes const BORDER_OFFSET = renderObject.type === SECTION ? borderWidth || 0 : childRectangle?.attributes?.borderWidth || 0 if (position === ComponentFixedPosition.TOP) { if (typeof newTop === 'number') { // BORDER_OFFSET make sure that we not only lift the components to fill // the inset area, but also hide the border on the top newTop -= statusBarHeight - BORDER_OFFSET * -1 } if (expandedTopFixedComponent !== layoutCacheKey) { if (typeof newHeight === 'number') { newHeight += statusBarHeight + BORDER_OFFSET if (renderObject.children) { // In case we're working with groups we need to // check if there is a child rectangle on the very top // if so, we expand it too for (const renderChild of renderObject.children) { if ( renderObject.type === GROUP && renderChild.type === SECTION && renderChild.layout.top === 0 ) { renderChild.layout.height = newHeight if (renderChild.children && renderChild.children.length > 0) { for (const child of renderChild.children) { child.layout.marginTop = statusBarHeight } } } else if (typeof statusBarHeight === 'number') { if ( typeof renderChild.layout.top === 'number' && renderObject.type === SECTION ) { renderChild.layout.marginTop = statusBarHeight const hasCustomLayoutForDeviceType = typeof renderChild.shared === 'object' && typeof renderChild.shared[deviceType] === 'boolean' && renderChild.shared[deviceType] === false if (hasCustomLayoutForDeviceType) { renderChild[deviceType].layout.marginTop = statusBarHeight } } } } } // this prevents we change the renderObject height multiple times // otherwise we would increase the height in every re-render setExpandedTopFixedComponent(layoutCacheKey) } } } else if (position === ComponentFixedPosition.BOTTOM) { let safeAreaBottom = 0 if (isPWA()) { const rootStyle = getComputedStyle(document.documentElement) const insetArea = rootStyle .getPropertyValue('--bottom-inset-area') .trim() .replace('px', '') safeAreaBottom = Number(insetArea) || 0 } const PWA_BOTTOM_OFFSET = safeAreaBottom if (typeof newBottom === 'number') { newBottom += Platform.OS === 'ios' ? -20 + BORDER_OFFSET * -1 : BORDER_OFFSET newBottom += PWA_BOTTOM_OFFSET } if ( expandedBottomFixedComponent !== layoutCacheKey && typeof newHeight === 'number' ) { // android devices do not contain a transparent // bottom notch as ios devices do newHeight += Platform.OS === 'android' ? 0 : statusBarHeight // this prevents we change the renderObject height multiple times // otherwise we would increase the height in every re-render setExpandedBottomFixedComponent(layoutCacheKey) } } return { newTop, newBottom, newHeight } }