diff --git a/example/src/screens/Playground/Playground.tsx b/example/src/screens/Playground/Playground.tsx index fe13cbc..43bdb6a 100644 --- a/example/src/screens/Playground/Playground.tsx +++ b/example/src/screens/Playground/Playground.tsx @@ -1,14 +1,35 @@ import React, { memo, useMemo } from 'react'; -import { View } from 'react-native'; +import { Alert, Text, View } from 'react-native'; import { HoldItem } from 'react-native-hold-menu'; import styles from './styles'; import { useAppContext } from '../../hooks/useAppContext'; import StyleGuide from '../../utilities/styleGuide'; +import { Pressable } from 'react-native'; interface PlaygroundProps {} +const PreviewComp = () => { + return ( + { + Alert.alert('sss'); + }} + style={{ + width: '100%', + height: '100%', + minWidth: 200, + minHeight: 200, + backgroundColor: 'green', + borderRadius: 16, + }} + > + Presss + + ); +}; + const Playground = ({}: PlaygroundProps) => { const { theme } = useAppContext(); @@ -88,12 +109,12 @@ const Playground = ({}: PlaygroundProps) => { - + - + - + @@ -105,7 +126,7 @@ const Playground = ({}: PlaygroundProps) => { - + @@ -123,7 +144,11 @@ const Playground = ({}: PlaygroundProps) => { - + diff --git a/example/src/screens/Whatsapp/MessageItem.tsx b/example/src/screens/Whatsapp/MessageItem.tsx index 7aee4ee..43209ff 100644 --- a/example/src/screens/Whatsapp/MessageItem.tsx +++ b/example/src/screens/Whatsapp/MessageItem.tsx @@ -1,15 +1,57 @@ import React, { memo, useMemo } from 'react'; -import { View, Text, StyleSheet } from 'react-native'; +import { View, Text, StyleSheet, Alert, TouchableOpacity } from 'react-native'; import StyleGuide from '../../utilities/styleGuide'; -import { MessageStyles } from './variables'; +import { + MessageStyles, + ReactionContainerStyles, + ReactionStyles, +} from './variables'; import { useAppContext } from '../../hooks/useAppContext'; // React Native Hold Menu Components -import { HoldItem } from 'react-native-hold-menu'; +import { HoldItem, PreviewComponentProps } from 'react-native-hold-menu'; import { IS_IOS } from '../../constants'; +const MessagePreview = ({ children, close }: PreviewComponentProps) => { + const { theme } = useAppContext(); + + const triggerReaction = () => { + Alert.alert('Reaction pressed'); + close(); + }; + + return ( + <> + + + + + + + + + {children} + + ); +}; + const MessageItemComp = ({ senderMenu, receiverMenu, @@ -57,6 +99,8 @@ const MessageItemComp = ({ maxWidth: '80%', }} closeOnTap + previewComponent={MessagePreview} + anchorEdge="bottom" > { StyleGuide.palette.whatsapp[theme].messageBackgroundReceiver, }; }; + +export const ReactionContainerStyles = (theme: 'light' | 'dark') => { + return { + borderRadius: StyleGuide.spacing, + backgroundColor: + StyleGuide.palette.whatsapp[theme].reactionContainerBackground, + marginBottom: StyleGuide.spacing, + flexGrow: 0, + flexDirection: 'row', + } as const; +}; + +export const ReactionStyles = (theme: 'light' | 'dark') => { + return { + borderRadius: StyleGuide.spacing * 2, + backgroundColor: StyleGuide.palette.whatsapp[theme].reactionBackground, + width: 30, + height: 30, + margin: StyleGuide.spacing, + }; +}; diff --git a/example/src/utilities/styleGuide.ts b/example/src/utilities/styleGuide.ts index 3ac1b84..cbf947a 100644 --- a/example/src/utilities/styleGuide.ts +++ b/example/src/utilities/styleGuide.ts @@ -26,12 +26,16 @@ const StyleGuide = { messageBackgroundSender: 'rgb(218, 248, 201)', messageBackgroundReceiver: '#FFF', messageText: '#474747', + reactionContainerBackground: '#FFF', + reactionBackground: '#474747', }, dark: { chatBackground: '#131415', messageBackgroundSender: '#075E54', messageBackgroundReceiver: '#2b2d2e', messageText: '#FFF', + reactionContainerBackground: '#2b2d2e', + reactionBackground: '#FFF', }, }, telegram: { diff --git a/src/components/holdItem/HoldItem.tsx b/src/components/holdItem/HoldItem.tsx index fc7dab4..b0a9cbc 100644 --- a/src/components/holdItem/HoldItem.tsx +++ b/src/components/holdItem/HoldItem.tsx @@ -1,5 +1,5 @@ import React, { memo, useMemo } from 'react'; -import { ViewProps } from 'react-native'; +import { TouchableWithoutFeedback, View, ViewProps } from 'react-native'; //#region reanimated & gesture handler import { @@ -44,17 +44,36 @@ import { WINDOW_HEIGHT, WINDOW_WIDTH, CONTEXT_MENU_STATE, + HOLD_ITEM_HIDE_DURATION, } from '../../constants'; import { useDeviceOrientation } from '../../hooks'; import styles from './styles'; -import type { HoldItemProps, GestureHandlerProps } from './types'; +import type { + HoldItemProps, + GestureHandlerProps, + PreviewComponentProps, +} from './types'; import styleGuide from '../../styleGuide'; import { useInternal } from '../../hooks'; //#endregion type Context = { didMeasureLayout: boolean }; +type Dimensions = { + width: number; + height: number; +}; + +type Rect = { + x: number; + y: number; +} & Dimensions; + +const DefaultPreview = ({ children }: PreviewComponentProps) => ( + {children} +); + const HoldItemComponent = ({ items, bottom, @@ -67,6 +86,8 @@ const HoldItemComponent = ({ closeOnTap, longPressMinDurationMs = 150, children, + previewComponent: Preview = DefaultPreview, + anchorEdge = 'top', }: HoldItemProps) => { //#region hooks const { state, menuProps, safeAreaInsets } = useInternal(); @@ -77,12 +98,19 @@ const HoldItemComponent = ({ const isActive = useSharedValue(false); const isAnimationStarted = useSharedValue(false); - const itemRectY = useSharedValue(0); - const itemRectX = useSharedValue(0); - const itemRectWidth = useSharedValue(0); - const itemRectHeight = useSharedValue(0); + const itemDimensions = useSharedValue({ + width: 0, + height: 0, + }); + + const previewRect = useSharedValue({ + x: 0, + y: 0, + width: 0, + height: 0, + }); + const itemScale = useSharedValue(1); - const transformValue = useSharedValue(0); const transformOrigin = useSharedValue( menuAnchorPosition || 'top-right' @@ -99,6 +127,7 @@ const HoldItemComponent = ({ //#region refs const containerRef = useAnimatedRef(); + const previewRef = useAnimatedRef(); //#endregion //#region functions @@ -124,25 +153,27 @@ const HoldItemComponent = ({ //#endregion //#region worklet functions - const activateAnimation = (ctx: any) => { + const activateAnimation = () => { 'worklet'; - if (!ctx.didMeasureLayout) { - const measured = measure(containerRef); - - itemRectY.value = measured.pageY; - itemRectX.value = measured.pageX; - itemRectHeight.value = measured.height; - itemRectWidth.value = measured.width; - - if (!menuAnchorPosition) { - const position = getTransformOrigin( - measured.pageX, - itemRectWidth.value, - deviceOrientation === 'portrait' ? WINDOW_WIDTH : WINDOW_HEIGHT, - bottom - ); - transformOrigin.value = position; - } + + const containerMeasures = measure(containerRef); + const previewMeasures = measure(previewRef); + + previewRect.value = { + x: containerMeasures.pageX, + y: containerMeasures.pageY, + width: previewMeasures.width, + height: previewMeasures.height, + }; + + if (!menuAnchorPosition) { + const position = getTransformOrigin( + previewRect.value.x, + previewRect.value.width, + deviceOrientation === 'portrait' ? WINDOW_WIDTH : WINDOW_HEIGHT, + bottom + ); + transformOrigin.value = position; } }; @@ -155,38 +186,69 @@ const HoldItemComponent = ({ const isAnchorPointTop = transformOrigin.value.includes('top'); let tY = 0; + let y = 0; + if (!disableMove) { + switch (anchorEdge) { + case 'top': + y = previewRect.value.y; + break; + case 'bottom': + let moveUp = previewRect.value.height - itemDimensions.value.height; + y = previewRect.value.y - moveUp; + // tY = tY + moveUp; + break; + } + if (isAnchorPointTop) { - const topTransform = - itemRectY.value + - itemRectHeight.value + + const topEdge = y - (safeAreaInsets?.top || 0); + + if (topEdge < 0) { + tY = -topEdge + styleGuide.spacing * 2; + } + + const bottomEdge = + y + + previewRect.value.height + menuHeight + - styleGuide.spacing + (safeAreaInsets?.bottom || 0); - tY = topTransform > height ? height - topTransform : 0; + if (bottomEdge > height) { + tY = height - bottomEdge; + } } else { - const bottomTransform = - itemRectY.value - menuHeight - (safeAreaInsets?.top || 0); - tY = - bottomTransform < 0 ? -bottomTransform + styleGuide.spacing * 2 : 0; + const topEdge = y - menuHeight - (safeAreaInsets?.top || 0); + + if (topEdge < 0) { + tY = -topEdge + styleGuide.spacing * 2; + } + + const bottomEdge = + y + previewRect.value.height + (safeAreaInsets?.bottom || 0); + + if (bottomEdge > height) { + tY = height - bottomEdge; + } } } - return tY; + + return { tY, y }; }; const setMenuProps = () => { 'worklet'; + const { tY, y } = calculateTransformValue(); + menuProps.value = { - itemHeight: itemRectHeight.value, - itemWidth: itemRectWidth.value, - itemY: itemRectY.value, - itemX: itemRectX.value, + itemHeight: previewRect.value.height, + itemWidth: previewRect.value.width, + itemY: y, + itemX: previewRect.value.x, anchorPosition: transformOrigin.value, menuHeight: menuHeight, items, - transformValue: transformValue.value, + transformValue: tY, actionParams: actionParams || {}, }; }; @@ -242,6 +304,20 @@ const HoldItemComponent = ({ ); }; + const previewTap = () => { + 'worklet'; + + if (closeOnTap) { + close(); + } + }; + + const close = () => { + 'worklet'; + + state.value = CONTEXT_MENU_STATE.END; + }; + /** * When use tap activation ("tap") and trying to tap multiple times, * scale animation is called again despite it is started. This causes a bug. @@ -266,8 +342,7 @@ const HoldItemComponent = ({ onActive: (_, context) => { if (canCallActivateFunctions()) { if (!context.didMeasureLayout) { - activateAnimation(context); - transformValue.value = calculateTransformValue(); + activateAnimation(); setMenuProps(); context.didMeasureLayout = true; } @@ -288,15 +363,6 @@ const HoldItemComponent = ({ } }, }); - - const overlayGestureEvent = useAnimatedGestureHandler< - TapGestureHandlerGestureEvent, - Context - >({ - onActive: _ => { - if (closeOnTap) state.value = CONTEXT_MENU_STATE.END; - }, - }); //#endregion //#region animated styles & props @@ -322,9 +388,13 @@ const HoldItemComponent = ({ const animatedPortalStyle = useAnimatedStyle(() => { const animateOpacity = () => - withDelay(HOLD_ITEM_TRANSFORM_DURATION, withTiming(0, { duration: 0 })); + withDelay( + HOLD_ITEM_TRANSFORM_DURATION, + withTiming(0, { duration: HOLD_ITEM_HIDE_DURATION }) + ); + + const { y, tY } = calculateTransformValue(); - let tY = calculateTransformValue(); const transformAnimation = () => disableMove ? 0 @@ -333,12 +403,8 @@ const HoldItemComponent = ({ : withTiming(-0.1, { duration: HOLD_ITEM_TRANSFORM_DURATION }); return { - zIndex: 10, - position: 'absolute', - top: itemRectY.value, - left: itemRectX.value, - width: itemRectWidth.value, - height: itemRectHeight.value, + top: y, + left: previewRect.value.x, opacity: isActive.value ? 1 : animateOpacity(), transform: [ { @@ -360,6 +426,13 @@ const HoldItemComponent = ({ const animatedPortalProps = useAnimatedProps(() => ({ pointerEvents: isActive.value ? 'auto' : 'none', })); + + const previewAnimatedStyle = useAnimatedStyle(() => { + return { + width: itemDimensions.value.width, + height: itemDimensions.value.height, + }; + }); //#endregion //#region animated effects @@ -405,37 +478,41 @@ const HoldItemComponent = ({ ); } - }, [activateOn, gestureEvent]); - - const PortalOverlay = useMemo(() => { - return () => ( - - - - ); - }, [overlayGestureEvent]); + }, [activateOn, gestureEvent, longPressMinDurationMs]); //#endregion //#region render return ( <> - + { + itemDimensions.value = { + width: event.nativeEvent.layout.width, + height: event.nativeEvent.layout.height, + }; + }} + > {children} - - {children} + + + + {children} + + + diff --git a/src/components/holdItem/index.ts b/src/components/holdItem/index.ts index 7a63b7e..6715034 100644 --- a/src/components/holdItem/index.ts +++ b/src/components/holdItem/index.ts @@ -1,2 +1,2 @@ export { default } from './HoldItem'; -export type { HoldItemProps } from './types'; +export type { HoldItemProps, PreviewComponentProps } from './types'; diff --git a/src/components/holdItem/styles.ts b/src/components/holdItem/styles.ts index e822b78..9526463 100644 --- a/src/components/holdItem/styles.ts +++ b/src/components/holdItem/styles.ts @@ -4,7 +4,7 @@ const styles = StyleSheet.create({ holdItem: { zIndex: 10, position: 'absolute' }, portalOverlay: { ...StyleSheet.absoluteFillObject, - zIndex: 15, + // zIndex: 15, }, }); diff --git a/src/components/holdItem/types.d.ts b/src/components/holdItem/types.d.ts index b5a4fd5..2a9a924 100644 --- a/src/components/holdItem/types.d.ts +++ b/src/components/holdItem/types.d.ts @@ -1,6 +1,12 @@ import { ViewStyle } from 'react-native'; import { MenuItemProps } from '../menu/types'; import { TransformOriginAnchorPosition } from '../../utils/calculations'; +import React from 'react'; + +export type PreviewComponentProps = { + children: React.ReactNode; + close: () => void; +}; export type HoldItemProps = { /** @@ -124,6 +130,12 @@ export type HoldItemProps = { * longPressMinDurationMs={250} */ longPressMinDurationMs?: number; + + previewComponent?: + | React.ComponentType + | React.ComponentType<{}>; + + anchorEdge?: 'top' | 'bottom'; }; export type GestureHandlerProps = { diff --git a/src/components/provider/Provider.tsx b/src/components/provider/Provider.tsx index 234ecf8..4e80ab8 100644 --- a/src/components/provider/Provider.tsx +++ b/src/components/provider/Provider.tsx @@ -1,6 +1,10 @@ import React, { memo, useEffect, useMemo } from 'react'; import { PortalProvider } from '@gorhom/portal'; -import Animated, { useSharedValue, useAnimatedReaction, runOnJS } from 'react-native-reanimated'; +import Animated, { + useSharedValue, + useAnimatedReaction, + runOnJS, +} from 'react-native-reanimated'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; // Components @@ -58,14 +62,12 @@ const ProviderComponent = ({ state => { switch (state) { case CONTEXT_MENU_STATE.ACTIVE: { - if (onOpen) - runOnJS(onOpen)(); - break + if (onOpen) runOnJS(onOpen)(); + break; } case CONTEXT_MENU_STATE.END: { - if (onClose) - runOnJS(onClose)(); - break + if (onClose) runOnJS(onClose)(); + break; } } }, diff --git a/src/components/provider/types.d.ts b/src/components/provider/types.d.ts index 3ed79c8..4e94130 100644 --- a/src/components/provider/types.d.ts +++ b/src/components/provider/types.d.ts @@ -28,6 +28,6 @@ export interface HoldMenuProviderProps { left: number; }; - onOpen?: function; - onClose?: function; + onOpen?: () => void; + onClose?: () => void; } diff --git a/src/constants.ts b/src/constants.ts index 688af63..86fa3c8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -3,6 +3,7 @@ import { Dimensions, Platform } from 'react-native'; const HOLD_ITEM_TRANSFORM_DURATION = 150; const HOLD_ITEM_SCALE_DOWN_VALUE = 0.95; const HOLD_ITEM_SCALE_DOWN_DURATION = 210; +const HOLD_ITEM_HIDE_DURATION = 50; const SPRING_CONFIGURATION = { damping: 33, @@ -46,6 +47,7 @@ export { HOLD_ITEM_TRANSFORM_DURATION, HOLD_ITEM_SCALE_DOWN_VALUE, HOLD_ITEM_SCALE_DOWN_DURATION, + HOLD_ITEM_HIDE_DURATION, SPRING_CONFIGURATION, SPRING_CONFIGURATION_MENU, MENU_TRANSFORM_ORIGIN_TOLERENCE, diff --git a/src/index.ts b/src/index.ts index 298a1ae..05035b7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,7 @@ -export { default as HoldItem } from './components/holdItem'; +export { + default as HoldItem, + PreviewComponentProps, +} from './components/holdItem'; export { default as HoldMenuProvider } from './components/provider'; export { default as HoldMenuFlatList } from './components/flatList'; export { default as HoldMenuIcon } from './components/icon';