react-native-gifted-chat
Version:
The most complete chat UI for React Native. Maintenance mode - active development continues in @kesha-antonov/react-native-chat
123 lines • 7.03 kB
JavaScript
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, useAnimatedReaction, runOnJS } from 'react-native-reanimated';
import { Day } from '../../../Day';
import stylesCommon from '../../../styles';
import { DAY_HANDOFF_OFFSET, DAY_MARGIN_TOP, DAY_PIN_OFFSET, DAY_PUSH_GAP, dayPositionScreenTop } from '../dayLayout';
import { DAY_DEBUG, useDayDebugOverlay } from './debug';
import styles from './styles';
import { useScrollGatedOpacity } from './useScrollGatedOpacity';
export * from './types';
export const DayAnimated = ({ scrolledY, daysPositions, listHeight, isScrollActive, floatingRenderedDate, renderDay, isLoading, ...rest }) => {
const containerHeight = useSharedValue(0);
const isLoadingAnim = useSharedValue(isLoading);
// Telegram only shows the floating date while scrolling; this opacity fades in on
// scroll start and out after the gesture fully ends.
const opacity = useScrollGatedOpacity(isScrollActive);
// Sort newest day first. Sorting by createdAt (stable) rather than measured y
// keeps the order deterministic even while cells are (re)measured mid-scroll, so
// the stuck-day selection below can't briefly jump to the wrong day.
const daysPositionsArray = useDerivedValue(() => Object.values(daysPositions.value).sort((a, b) => {
'worklet';
return b.createdAt - a.createdAt;
}));
const [createdAt, setCreatedAt] = useState();
// Telegram-style sticky day header (iOS section-header behaviour).
//
// The list is inverted: older days sit higher on screen, newer days lower.
// `daysPositionsArray` is sorted by createdAt (newest first), so index 0 is the
// newest day. Each separator's on-screen top edge is `dayPositionScreenTop`; the
// separator's pill renders DAY_MARGIN_TOP below that, and the floating header
// overrides that margin to 0, so its pinned pill (top = DAY_PIN_OFFSET) lines up
// with an inline separator at separatorScreenTop === DAY_HANDOFF_OFFSET.
//
// A day becomes "stuck" (shown by the floating header) the instant its separator
// reaches the handoff line. At that same pixel the inline separator hard-cuts off
// (see Item) and the floating hard-cuts on - the date hands off floating<->inline
// with no fade and no duplicate. The floating is then positioned off the next
// (newer) day's separator so it slides pixel by pixel: down from the top edge as
// an older day takes over (scrolling up), or up and off as a newer day rises
// (scrolling down), keeping a DAY_PUSH_GAP margin between the two pills.
const sticky = useDerivedValue(() => {
'worklet';
const days = daysPositionsArray.value;
const n = days.length;
if (n === 0)
return { top: DAY_PIN_OFFSET, createdAt: undefined, curSep: NaN };
const scrolledTop = listHeight.value + scrolledY.value;
let idx = n - 1;
for (let i = 0; i < n; i++)
if (dayPositionScreenTop(scrolledTop, days[i]) <= DAY_HANDOFF_OFFSET) {
idx = i;
break;
}
const current = days[idx];
const curSep = dayPositionScreenTop(scrolledTop, current);
if (isLoadingAnim.value)
return { top: -containerHeight.value, createdAt: current.createdAt, curSep };
let top = DAY_PIN_OFFSET;
if (idx > 0) {
const nextSep = dayPositionScreenTop(scrolledTop, days[idx - 1]);
top = Math.min(DAY_PIN_OFFSET, nextSep + DAY_MARGIN_TOP - containerHeight.value - DAY_PUSH_GAP);
}
return { top, createdAt: current.createdAt, curSep };
}, [daysPositionsArray, listHeight, scrolledY, containerHeight, isLoadingAnim]);
const style = useAnimatedStyle(() => ({
top: sticky.value.top,
}), [sticky]);
const contentStyle = useAnimatedStyle(() => {
// The scroll-gated fade (see useScrollGatedOpacity); DAY_DEBUG.forceOpacity pins
// it to 1 so the slide/push can be studied without the fade.
const fade = DAY_DEBUG.forceOpacity ? 1 : opacity.value;
// stuckGate hides the floating header when no day is actually stuck above the
// pin - i.e. at the top of history, once the oldest day's own separator drops
// back below the pin (e.g. the "Load earlier" button scrolls in). It is a hard
// step (not a fade) so the date hands off floating->inline at the same pixel
// with no fading duplicate over the loader, mirroring the inline separator's
// hard cutoff.
const stuckGate = sticky.value.curSep <= DAY_HANDOFF_OFFSET ? 1 : 0;
// renderGate hides the header while its rendered text hasn't caught up to the
// worklet's current stuck day (the ~1-frame JS-thread lag after a day change).
// The inline separator stays up during this frame and shows the correct date, so
// the header never flashes the previous date when scrolling into a newer day.
const renderGate = sticky.value.createdAt === floatingRenderedDate.value ? 1 : 0;
return { opacity: fade * stuckGate * renderGate };
}, [opacity, sticky, floatingRenderedDate]);
const handleLayout = useCallback(({ nativeEvent }) => {
containerHeight.value = nativeEvent.layout.height;
}, [containerHeight]);
useAnimatedReaction(() => sticky.value.createdAt, (value, prevValue) => {
if (value && value !== prevValue)
runOnJS(setCreatedAt)(value);
}, [sticky]);
const debugOverlay = useDayDebugOverlay(() => {
'worklet';
const v = sticky.value;
return `top=${Math.round(v.top)} cur=${Math.round(v.curSep)} ch=${Math.round(containerHeight.value)} load=${isLoadingAnim.value ? 1 : 0}`;
}, [sticky, containerHeight, isLoadingAnim]);
useEffect(() => {
isLoadingAnim.value = isLoading;
}, [isLoadingAnim, isLoading]);
// Publish the date the header is actually rendering so the inline separators (and
// the header's own renderGate) know when the text has caught up to the worklet.
useEffect(() => {
floatingRenderedDate.value = createdAt;
}, [createdAt, floatingRenderedDate]);
const dayContent = useMemo(() => {
if (!createdAt)
return null;
return renderDay
? renderDay({ ...rest, createdAt, isAnimated: true })
: <Day {...rest} containerStyle={[styles.dayAnimatedDayContainerStyle, rest.containerStyle]} createdAt={createdAt} isAnimated/>;
}, [createdAt, renderDay, rest]);
if (!createdAt)
return debugOverlay;
return (<>
{debugOverlay}
<Animated.View style={[stylesCommon.centerItems, styles.dayAnimated, style]} onLayout={handleLayout} pointerEvents='none'>
<Animated.View style={contentStyle} pointerEvents='none'>
{dayContent}
</Animated.View>
</Animated.View>
</>);
};
//# sourceMappingURL=index.js.map