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';