import React, { useEffect, useMemo } from 'react';
import {
I18nManager,
Platform,
Pressable,
StyleSheet,
StyleProp,
useWindowDimensions,
View,
ViewStyle,
} from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
clamp,
runOnJS,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withSpring,
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { PortalHost } from 'react-native-teleport';
import { ClosingPortalHostsLayer } from './ClosingPortalHostsLayer';
import {
closeOverlay,
finalizeCloseOverlay,
registerOverlaySharedValueController,
Rect,
useOverlayController,
} from '../../state-store';
import { useComponentsContext } from '../componentsContext/ComponentsContext';
import { useTheme } from '../themeContext/ThemeContext';
const DURATION = 300;
export const DefaultMessageOverlayBackground = () => {
const {
theme: { semantics },
} = useTheme();
return (
);
};
export type MessageActionsProps = {
bottomItemStyle: StyleProp;
hostStyle: StyleProp;
portalHostStyle: StyleProp;
topItemStyle: StyleProp;
};
export const MessageOverlayHostLayer = () => {
const { MessageActions, MessageOverlayBackground } = useComponentsContext();
const { id, closing } = useOverlayController();
const insets = useSafeAreaInsets();
const { height: screenH } = useWindowDimensions();
const messageH = useSharedValue(undefined);
const topH = useSharedValue(undefined);
const bottomH = useSharedValue(undefined);
const closeCorrectionY = useSharedValue(0);
const topInset = insets.top;
// Due to edge-to-edge in combination with various libraries, Android sometimes reports
// the insets to be 0. If that's the case, we use this as an escape hatch to offset the bottom
// of the overlay so that it doesn't collide with the navigation bar. Worst case scenario,
// if the navigation bar is actually 0 - we end up animating a little bit further.
const bottomInset = insets.bottom === 0 && Platform.OS === 'android' ? 60 : insets.bottom;
const isActive = !!id;
const padding = 8;
const minY = topInset + padding;
const maxY = screenH - bottomInset - padding;
const backdrop = useSharedValue(0);
const closeCoverOpacity = useSharedValue(0);
useEffect(
() =>
registerOverlaySharedValueController({
incrementCloseCorrectionY: (deltaY) => {
closeCorrectionY.value += deltaY;
},
resetCloseCorrectionY: () => {
closeCorrectionY.value = 0;
},
reset: () => {
messageH.value = undefined;
topH.value = undefined;
bottomH.value = undefined;
closeCorrectionY.value = 0;
},
setBottomH: (rect) => {
bottomH.value = rect;
},
setMessageH: (rect) => {
messageH.value = rect;
},
setTopH: (rect) => {
topH.value = rect;
},
}),
[bottomH, closeCorrectionY, messageH, topH],
);
useEffect(() => {
const target = isActive && !closing ? 1 : 0;
backdrop.value = withSpring(target, { duration: DURATION + target * 100 }, (finished) => {
if (finished && closing) {
runOnJS(finalizeCloseOverlay)();
}
});
closeCoverOpacity.value = withSpring(closing ? 1 : 0, { duration: DURATION });
}, [isActive, closing, backdrop, closeCoverOpacity]);
const backdropStyle = useAnimatedStyle(() => ({
opacity: backdrop.value,
}));
const OverlayBackground = MessageOverlayBackground;
const messageShiftY = useDerivedValue(() => {
if (!messageH.value || !topH.value || !bottomH.value) return 0;
const anchorY = messageH.value.y;
const msgH = messageH.value.h;
const minTop = minY + topH.value.h;
const maxTopWithBottom = maxY - (msgH + bottomH.value.h);
const canFitBottomWithoutOverlap = minTop <= maxTopWithBottom;
const solvedTop = canFitBottomWithoutOverlap
? clamp(anchorY, minTop, Math.max(minTop, maxTopWithBottom))
: minTop;
return solvedTop - anchorY;
});
const bottomShiftY = useDerivedValue(() => {
if (!messageH.value || !topH.value || !bottomH.value) return 0;
const anchorMessageTop = messageH.value.y;
const msgH = messageH.value.h;
const minMessageTop = minY + topH.value.h;
const maxMessageTopWithBottom = maxY - (msgH + bottomH.value.h);
const canFitBottomWithoutOverlap = minMessageTop <= maxMessageTopWithBottom;
const solvedMessageTop = canFitBottomWithoutOverlap
? clamp(anchorMessageTop, minMessageTop, Math.max(minMessageTop, maxMessageTopWithBottom))
: minMessageTop;
const solvedBottomTop = Math.min(solvedMessageTop + msgH, maxY - bottomH.value.h);
return solvedBottomTop - bottomH.value.y;
});
const topItemStyle = useAnimatedStyle(() => {
if (!topH.value) return { opacity: 0, height: 0, width: 0 };
const translateY = isActive ? (closing ? closeCorrectionY.value : messageShiftY.value) : 0;
const horizontalPosition = I18nManager.isRTL ? { right: topH.value.x } : { left: topH.value.x };
return {
height: topH.value.h,
opacity: 1,
position: 'absolute',
top: topH.value.y,
transform: [
{ scale: backdrop.value },
{ translateY: withSpring(translateY, { duration: DURATION }) },
],
width: topH.value.w,
...horizontalPosition,
};
});
const bottomItemStyle = useAnimatedStyle(() => {
if (!bottomH.value) return { opacity: 0, height: 0, width: 0 };
const translateY = isActive ? (closing ? closeCorrectionY.value : bottomShiftY.value) : 0;
const horizontalPosition = I18nManager.isRTL
? { right: bottomH.value.x }
: { left: bottomH.value.x };
return {
height: bottomH.value.h,
opacity: 1,
position: 'absolute',
top: bottomH.value.y,
transform: [
{ scale: backdrop.value },
{ translateY: withSpring(translateY, { duration: DURATION }) },
],
width: bottomH.value.w,
...horizontalPosition,
};
});
const hostStyle = useAnimatedStyle(() => {
if (!messageH.value) return { height: 0 };
const translateY = isActive ? (closing ? closeCorrectionY.value : messageShiftY.value) : 0;
return {
height: messageH.value.h,
left: messageH.value.x,
position: 'absolute',
top: messageH.value.y,
transform: [{ translateY: withSpring(translateY, { duration: DURATION }) }],
width: messageH.value.w,
};
});
const tap = useMemo(
() =>
Gesture.Tap()
.onTouchesDown((e, state) => {
const t = e.allTouches[0];
if (!t || !topH || !bottomH) return;
const x = t.x;
const y = t.y;
const messageYShift = messageShiftY.value; // overlay shift for top + message
const bottomYShift = bottomShiftY.value; // overlay shift for bottom
const top = topH.value;
if (top) {
const topY = top.y + messageYShift;
if (x >= top.x && x <= top.x + top.w && y >= topY && y <= topY + top.h) {
state.fail();
return;
}
}
const bot = bottomH.value;
if (bot) {
const botY = bot.y + bottomYShift;
if (x >= bot.x && x <= bot.x + bot.w && y >= botY && y <= botY + bot.h) {
state.fail();
return;
}
}
})
.onEnd(() => {
runOnJS(closeOverlay)();
}),
[bottomH, bottomShiftY, messageShiftY, topH],
);
return (
{isActive ? (
) : null}
{isActive ? (
) : null}
{MessageActions ? (
) : (
<>
>
)}
);
};
const styles = StyleSheet.create({
absoluteFill: {
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
});