import React, { useCallback, useMemo } from 'react' import { LayoutChangeEvent, View } from 'react-native' import Animated, { interpolate, useAnimatedStyle, useDerivedValue, useSharedValue } from 'react-native-reanimated' import { Day } from '../../../Day' import { Message, MessageProps } from '../../../Message' import { IMessage } from '../../../Models' import { isSameDay } from '../../../utils' import { DaysPositions } from '../../types' import { ItemProps } from './types' export * from './types' // y-position of current scroll position relative to the bottom of the day container. (since we have inverted list it is bottom) export const useAbsoluteScrolledPositionToBottomOfDay = (listHeight: { value: number }, scrolledY: { value: number }, containerHeight: { value: number }, dayBottomMargin: number, dayTopOffset: number) => { const absoluteScrolledPositionToBottomOfDay = useDerivedValue(() => listHeight.value + scrolledY.value - containerHeight.value - dayBottomMargin - dayTopOffset , [listHeight, scrolledY, containerHeight, dayBottomMargin, dayTopOffset]) return absoluteScrolledPositionToBottomOfDay } export const useRelativeScrolledPositionToBottomOfDay = ( listHeight: { value: number }, scrolledY: { value: number }, daysPositions: { value: DaysPositions }, containerHeight: { value: number }, dayBottomMargin: number, dayTopOffset: number, createdAt?: number ) => { const dayMarginTop = useMemo(() => 5, []) const absoluteScrolledPositionToBottomOfDay = useAbsoluteScrolledPositionToBottomOfDay(listHeight, scrolledY, containerHeight, dayBottomMargin, dayTopOffset) // find current day position by scrolled position const currentDayPosition = useDerivedValue(() => { 'worklet' // When createdAt is provided (called from AnimatedDayWrapper for a specific message), // directly find the day position by createdAt without sorting the entire array. // This avoids O(n log n) sorting and O(n) search for each message item. if (createdAt != null) { const values = Object.values(daysPositions.value) for (let i = 0; i < values.length; i++) if (values[i].createdAt === createdAt) return values[i] } // Fallback: sort and search when createdAt is not provided (e.g., from DayAnimated) const sortedArray = Object.values(daysPositions.value).sort((a, b) => { 'worklet' return a.y - b.y }) for (let i = 0; i < sortedArray.length; i++) { const day = sortedArray[i] const dayPosition = day.y + day.height if (absoluteScrolledPositionToBottomOfDay.value < dayPosition || i === sortedArray.length - 1) return day } return undefined }, [daysPositions, absoluteScrolledPositionToBottomOfDay, createdAt]) const relativeScrolledPositionToBottomOfDay = useDerivedValue(() => { const scrolledBottomY = listHeight.value + scrolledY.value - ( (currentDayPosition.value?.y ?? 0) + (currentDayPosition.value?.height ?? 0) + dayMarginTop ) return scrolledBottomY }, [listHeight, scrolledY, currentDayPosition, dayMarginTop]) return relativeScrolledPositionToBottomOfDay } const DayWrapper = (props: MessageProps) => { const { renderDay: renderDayProp, currentMessage, previousMessage, } = props if (!currentMessage?.createdAt || isSameDay(currentMessage, previousMessage)) return null const { /* eslint-disable @typescript-eslint/no-unused-vars */ containerStyle, onMessageLayout, /* eslint-enable @typescript-eslint/no-unused-vars */ ...rest } = props return ( { renderDayProp ? renderDayProp({ ...rest, createdAt: currentMessage.createdAt }) : } ) } const AnimatedDayWrapper = (props: ItemProps) => { const { scrolledY, daysPositions, listHeight, ...rest } = props const dayContainerHeight = useSharedValue(0) const dayTopOffset = useMemo(() => 10, []) const dayBottomMargin = useMemo(() => 10, []) const createdAt = useMemo(() => new Date(props.currentMessage.createdAt).getTime() , [props.currentMessage.createdAt]) const relativeScrolledPositionToBottomOfDay = useRelativeScrolledPositionToBottomOfDay(listHeight, scrolledY, daysPositions, dayContainerHeight, dayBottomMargin, dayTopOffset, createdAt) const handleLayoutDayContainer = useCallback(({ nativeEvent }: LayoutChangeEvent) => { dayContainerHeight.value = nativeEvent.layout.height }, [dayContainerHeight]) const style = useAnimatedStyle(() => ({ opacity: interpolate( relativeScrolledPositionToBottomOfDay.value, [ -dayTopOffset, -0.0001, 0, dayContainerHeight.value + dayTopOffset, ], [ 0, 0, 1, 1, ], 'clamp' ), }), [relativeScrolledPositionToBottomOfDay, dayContainerHeight, dayTopOffset]) return ( {...rest as MessageProps} /> ) } export const Item = (props: ItemProps) => { const { renderMessage: renderMessageProp, isDayAnimationEnabled, reply, /* eslint-disable @typescript-eslint/no-unused-vars */ scrolledY: _scrolledY, daysPositions: _daysPositions, listHeight: _listHeight, /* eslint-enable @typescript-eslint/no-unused-vars */ ...rest } = props // Transform reply props for Message and Bubble const messageProps = useMemo(() => ({ ...rest, // Swipe to reply for Message component swipeToReply: reply?.swipe, // Message reply styling for Bubble component messageReply: reply ? { renderMessageReply: reply.renderMessageReply, onPress: reply.onPress, ...reply.messageStyle, } : undefined, }), [rest, reply]) return ( // do not remove key. it helps to get correct position of the day container {isDayAnimationEnabled ? {...props} /> : {...messageProps as MessageProps} />} { renderMessageProp ? renderMessageProp(messageProps as MessageProps) : {...messageProps as MessageProps} /> } ) }