From db03f52900009b38fbf0c803a1374096589be305 Mon Sep 17 00:00:00 2001 From: Alexandre Tachau Date: Sun, 8 Mar 2026 23:35:46 +0100 Subject: [PATCH 1/7] Gesture detector & animation based on Pan --- src/components/feed/ShareModal.tsx | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/components/feed/ShareModal.tsx b/src/components/feed/ShareModal.tsx index d22ad60..15d0353 100644 --- a/src/components/feed/ShareModal.tsx +++ b/src/components/feed/ShareModal.tsx @@ -5,6 +5,8 @@ import React from 'react'; import { Dimensions, Modal, Pressable, Share, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import tw from 'twrnc'; +import { GestureHandlerRootView, Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window'); const TAB_BAR_HEIGHT = 60; @@ -85,9 +87,28 @@ export default function ShareModal({ visible, item, onClose }) { }, ]; + const translateY = useSharedValue(0); + const context = useSharedValue({y: 0}); + + const gesture = Gesture.Pan().onStart(() => { + context.value = { y: translateY.value }; + }).onUpdate((event) => { + translateY.value = event.translationY + context.value.y; + translateY.value = Math.max(translateY.value, 0); + console.log(translateY.value) + }); + + const swipeStyle = useAnimatedStyle(() => { + return { + transform: [{ translateY: translateY.value}] + } + }); + return ( - + + + Cancel - + + + ); } From 48043bd25ef3276ddd5b24f0ba11c2cc97e41863 Mon Sep 17 00:00:00 2001 From: taffroi Date: Tue, 10 Mar 2026 22:28:20 +0100 Subject: [PATCH 2/7] Added logic & animation --- src/components/feed/ShareModal.tsx | 141 ++++++++++++++++------------- 1 file changed, 79 insertions(+), 62 deletions(-) diff --git a/src/components/feed/ShareModal.tsx b/src/components/feed/ShareModal.tsx index 15d0353..8fd1567 100644 --- a/src/components/feed/ShareModal.tsx +++ b/src/components/feed/ShareModal.tsx @@ -1,12 +1,12 @@ import { useTheme } from '@/contexts/ThemeContext'; import { shareContent } from '@/utils/sharer'; import { Ionicons } from '@expo/vector-icons'; -import React from 'react'; +import React, { useEffect } from 'react'; import { Dimensions, Modal, Pressable, Share, Text, TouchableOpacity, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import tw from 'twrnc'; import { GestureHandlerRootView, Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window'); const TAB_BAR_HEIGHT = 60; @@ -52,6 +52,37 @@ export default function ShareModal({ visible, item, onClose }) { const insets = useSafeAreaInsets(); const { colorScheme } = useTheme(); + const translateY = useSharedValue(0); + const context = useSharedValue({ y: 0 }); + const MAXtranslateY = 320; + + const swipeStyle = useAnimatedStyle(() => { + return { + transform: [{ translateY: translateY.value }], + }; + }); + + const gesture = Gesture.Pan() + .onStart(() => { + context.value = { y: translateY.value }; + }) + .onUpdate((event) => { + translateY.value = event.translationY + context.value.y; + translateY.value = Math.max(Math.min(translateY.value, MAXtranslateY), 0); + console.log(translateY.value); + }) + .onEnd(() => { + if (translateY.value < 80) { + translateY.value = withTiming(0); + } else { + translateY.value = withTiming(320); + } + }); + + useEffect(() => { + translateY.value = withTiming(0); + }, []); + if (!item) return null; const handleRepost = async () => { @@ -87,71 +118,57 @@ export default function ShareModal({ visible, item, onClose }) { }, ]; - const translateY = useSharedValue(0); - const context = useSharedValue({y: 0}); - - const gesture = Gesture.Pan().onStart(() => { - context.value = { y: translateY.value }; - }).onUpdate((event) => { - translateY.value = event.translationY + context.value.y; - translateY.value = Math.max(translateY.value, 0); - console.log(translateY.value) - }); - - const swipeStyle = useAnimatedStyle(() => { - return { - transform: [{ translateY: translateY.value}] - } - }); - return ( - - - - - - - - Share to - - - - {shareOptions.map((option, index) => ( + + + + + + + + Share to + + + + {shareOptions.map((option, index) => ( + + + + + + {option.label} + + + ))} + + - - - - - {option.label} + style={tw`mt-3 py-4 items-center border-t border-gray-100 dark:border-gray-800`} + onPress={onClose}> + + Cancel - ))} - - - - Cancel - - - - + + + ); From 15543c1eb871554b6a42091b28cbb6b1338e9c71 Mon Sep 17 00:00:00 2001 From: Alexandre Tachau Date: Fri, 13 Mar 2026 16:18:01 +0100 Subject: [PATCH 3/7] test --- src/components/feed/ShareModal.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/feed/ShareModal.tsx b/src/components/feed/ShareModal.tsx index 8fd1567..b15fa82 100644 --- a/src/components/feed/ShareModal.tsx +++ b/src/components/feed/ShareModal.tsx @@ -3,10 +3,10 @@ import { shareContent } from '@/utils/sharer'; import { Ionicons } from '@expo/vector-icons'; import React, { useEffect } from 'react'; import { Dimensions, Modal, Pressable, Share, Text, TouchableOpacity, View } from 'react-native'; +import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import tw from 'twrnc'; -import { GestureHandlerRootView, Gesture, GestureDetector } from 'react-native-gesture-handler'; -import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window'); const TAB_BAR_HEIGHT = 60; @@ -72,10 +72,14 @@ export default function ShareModal({ visible, item, onClose }) { console.log(translateY.value); }) .onEnd(() => { - if (translateY.value < 80) { - translateY.value = withTiming(0); + if (translateY.value > 160) { + // Animation de fermeture fluide avant d'appeler onClose + translateY.value = withTiming(MAXtranslateY, { duration: 200 }, () => { + // Appel onClose uniquement après l'animation + onClose(); + }); } else { - translateY.value = withTiming(320); + translateY.value = withTiming(0, { duration: 200 }); } }); From 3e52a2d239c96c4c700fea2e6425c81113c21270 Mon Sep 17 00:00:00 2001 From: Alexandre Tachau Date: Fri, 13 Mar 2026 17:17:52 +0100 Subject: [PATCH 4/7] ShareModal Swipe: Closes based on velocity, quicker transitions, fixes crashes & removed unnecessary padding to cancel --- src/components/feed/ShareModal.tsx | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/components/feed/ShareModal.tsx b/src/components/feed/ShareModal.tsx index b15fa82..4d9babb 100644 --- a/src/components/feed/ShareModal.tsx +++ b/src/components/feed/ShareModal.tsx @@ -6,6 +6,7 @@ import { Dimensions, Modal, Pressable, Share, Text, TouchableOpacity, View } fro import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { scheduleOnRN } from 'react-native-worklets'; import tw from 'twrnc'; const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window'); @@ -54,7 +55,7 @@ export default function ShareModal({ visible, item, onClose }) { const translateY = useSharedValue(0); const context = useSharedValue({ y: 0 }); - const MAXtranslateY = 320; + const closeTranslateY = 340; // I didn't find a way to get the View height, fix this if possible ! const swipeStyle = useAnimatedStyle(() => { return { @@ -63,29 +64,24 @@ export default function ShareModal({ visible, item, onClose }) { }); const gesture = Gesture.Pan() - .onStart(() => { - context.value = { y: translateY.value }; - }) .onUpdate((event) => { - translateY.value = event.translationY + context.value.y; - translateY.value = Math.max(Math.min(translateY.value, MAXtranslateY), 0); - console.log(translateY.value); + translateY.value = Math.max(Math.min(event.translationY, closeTranslateY), 0); }) - .onEnd(() => { - if (translateY.value > 160) { - // Animation de fermeture fluide avant d'appeler onClose - translateY.value = withTiming(MAXtranslateY, { duration: 200 }, () => { - // Appel onClose uniquement après l'animation - onClose(); + .onEnd((event) => { + if (event.velocityY > 100) { + translateY.value = withTiming(closeTranslateY, { duration: 150 }, () => { + // Closes after the animation, scheduleOnRN necessary or else it crashes the app + scheduleOnRN(onClose); }); } else { - translateY.value = withTiming(0, { duration: 200 }); + translateY.value = withTiming(0, { duration: 150 }); } }); + // Reset ShareModal position at each opening useEffect(() => { - translateY.value = withTiming(0); - }, []); + translateY.value = 0; + }); if (!item) return null; @@ -131,7 +127,7 @@ export default function ShareModal({ visible, item, onClose }) { Date: Sat, 14 Mar 2026 13:36:02 +0100 Subject: [PATCH 5/7] New BottomSheet composent, make it use on ShareModal & OtherModal --- src/components/feed/OtherModal.tsx | 55 +++++++----------------- src/components/feed/ShareModal.tsx | 54 ++--------------------- src/components/ui/bottomSheet.tsx | 69 ++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 90 deletions(-) create mode 100644 src/components/ui/bottomSheet.tsx diff --git a/src/components/feed/OtherModal.tsx b/src/components/feed/OtherModal.tsx index 0429fc6..e06be53 100644 --- a/src/components/feed/OtherModal.tsx +++ b/src/components/feed/OtherModal.tsx @@ -4,10 +4,10 @@ import { videoDelete } from '@/utils/requests'; import { Ionicons } from '@expo/vector-icons'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'expo-router'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { Alert, Dimensions, Modal, Pressable, Text, TouchableOpacity, View } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import tw from 'twrnc'; +import BottomSheet from '@/components/ui/bottomSheet'; const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window'); const TAB_BAR_HEIGHT = 60; @@ -19,7 +19,6 @@ export default function OtherModal({ onPlaybackSpeedChange, currentPlaybackRate = 1.0, }) { - const insets = useSafeAreaInsets(); const router = useRouter(); const { colorScheme } = useTheme(); const [showPlaybackSpeed, setShowPlaybackSpeed] = useState(false); @@ -112,24 +111,13 @@ export default function OtherModal({ if (showPlaybackSpeed) { return ( - setShowPlaybackSpeed(false)}> - - setShowPlaybackSpeed(false)} - /> - - + { + setShowPlaybackSpeed(false); + onClose(); + }} + > Playback Speed @@ -156,12 +144,12 @@ export default function OtherModal({ setShowPlaybackSpeed(false)}> + onPress={() => { + setShowPlaybackSpeed(false); + }}> Cancel - - - + ); } @@ -216,18 +204,7 @@ export default function OtherModal({ } return ( - - - - - - + {options.map((option, index) => ( @@ -261,8 +238,6 @@ export default function OtherModal({ onPress={onClose}> Cancel - - - + ); } diff --git a/src/components/feed/ShareModal.tsx b/src/components/feed/ShareModal.tsx index 4d9babb..9164013 100644 --- a/src/components/feed/ShareModal.tsx +++ b/src/components/feed/ShareModal.tsx @@ -8,6 +8,7 @@ import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-na import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { scheduleOnRN } from 'react-native-worklets'; import tw from 'twrnc'; +import BottomSheet from '@/components/ui/bottomSheet'; const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window'); const TAB_BAR_HEIGHT = 60; @@ -50,39 +51,8 @@ type CommentReplyLikePayload = { }; export default function ShareModal({ visible, item, onClose }) { - const insets = useSafeAreaInsets(); const { colorScheme } = useTheme(); - const translateY = useSharedValue(0); - const context = useSharedValue({ y: 0 }); - const closeTranslateY = 340; // I didn't find a way to get the View height, fix this if possible ! - - const swipeStyle = useAnimatedStyle(() => { - return { - transform: [{ translateY: translateY.value }], - }; - }); - - const gesture = Gesture.Pan() - .onUpdate((event) => { - translateY.value = Math.max(Math.min(event.translationY, closeTranslateY), 0); - }) - .onEnd((event) => { - if (event.velocityY > 100) { - translateY.value = withTiming(closeTranslateY, { duration: 150 }, () => { - // Closes after the animation, scheduleOnRN necessary or else it crashes the app - scheduleOnRN(onClose); - }); - } else { - translateY.value = withTiming(0, { duration: 150 }); - } - }); - - // Reset ShareModal position at each opening - useEffect(() => { - translateY.value = 0; - }); - if (!item) return null; const handleRepost = async () => { @@ -119,20 +89,8 @@ export default function ShareModal({ visible, item, onClose }) { ]; return ( - - - - - - - - + Share to @@ -166,10 +124,6 @@ export default function ShareModal({ visible, item, onClose }) { Cancel - - - - - + ); } diff --git a/src/components/ui/bottomSheet.tsx b/src/components/ui/bottomSheet.tsx new file mode 100644 index 0000000..4edec50 --- /dev/null +++ b/src/components/ui/bottomSheet.tsx @@ -0,0 +1,69 @@ +import { useTheme } from '@/contexts/ThemeContext'; +import React, { useEffect } from 'react'; +import { Dimensions, Modal, Pressable, Share, Text, TouchableOpacity, View } from 'react-native'; +import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { scheduleOnRN } from 'react-native-worklets'; +import tw from 'twrnc'; + +const { height: SCREEN_HEIGHT, width: SCREEN_WIDTH } = Dimensions.get('window'); +const TAB_BAR_HEIGHT = 60; + +const BottomSheet = ({ visible, onClose, children }) => { + const insets = useSafeAreaInsets(); + const { colorScheme } = useTheme(); + + const translateY = useSharedValue(0); + const closeTranslateY = 340; // I didn't find a way to get the View height, fix this if possible ! + + const swipeStyle = useAnimatedStyle(() => { + return { + transform: [{ translateY: translateY.value }], + }; + }); + + const gesture = Gesture.Pan() + .onUpdate((event) => { + translateY.value = Math.max(Math.min(event.translationY, closeTranslateY), 0); + }) + .onEnd((event) => { + if (event.velocityY > 100) { + translateY.value = withTiming(closeTranslateY, { duration: 150 }, () => { + // Closes after the animation, scheduleOnRN necessary or else it crashes the app + scheduleOnRN(onClose); + }); + } else { + translateY.value = withTiming(0, { duration: 150 }); + } + }); + + // Reset ShareModal position at each opening + useEffect(() => { + translateY.value = 0; + }); + + return ( + + + + + + + + {children} + + + + + + ); +} + +export default BottomSheet; From c548a6754df985258a4e319f6d80cc2bb3f65e06 Mon Sep 17 00:00:00 2001 From: Alexandre Tachau Date: Sat, 14 Mar 2026 14:13:29 +0100 Subject: [PATCH 6/7] BottomSheet: close position further down --- src/components/ui/bottomSheet.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ui/bottomSheet.tsx b/src/components/ui/bottomSheet.tsx index 4edec50..ec18c64 100644 --- a/src/components/ui/bottomSheet.tsx +++ b/src/components/ui/bottomSheet.tsx @@ -15,7 +15,7 @@ const BottomSheet = ({ visible, onClose, children }) => { const { colorScheme } = useTheme(); const translateY = useSharedValue(0); - const closeTranslateY = 340; // I didn't find a way to get the View height, fix this if possible ! + const closeTranslateY = 600; // I didn't find a way to get the View height, fix this if possible ! const swipeStyle = useAnimatedStyle(() => { return { From c40dd808e7c5c53d59d29f84f1f8e1475cf5d6f5 Mon Sep 17 00:00:00 2001 From: Alexandre Tachau Date: Sat, 14 Mar 2026 15:34:37 +0100 Subject: [PATCH 7/7] BottomSheet: content height calculated + max height is 85% of screen --- src/components/ui/bottomSheet.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/ui/bottomSheet.tsx b/src/components/ui/bottomSheet.tsx index ec18c64..90b56a9 100644 --- a/src/components/ui/bottomSheet.tsx +++ b/src/components/ui/bottomSheet.tsx @@ -1,5 +1,5 @@ import { useTheme } from '@/contexts/ThemeContext'; -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { Dimensions, Modal, Pressable, Share, Text, TouchableOpacity, View } from 'react-native'; import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; @@ -15,7 +15,8 @@ const BottomSheet = ({ visible, onClose, children }) => { const { colorScheme } = useTheme(); const translateY = useSharedValue(0); - const closeTranslateY = 600; // I didn't find a way to get the View height, fix this if possible ! + const [contentHeight, setContentHeight] = useState(0); + const closeTranslateY = contentHeight || 900; const swipeStyle = useAnimatedStyle(() => { return { @@ -51,13 +52,18 @@ const BottomSheet = ({ visible, onClose, children }) => { + ]} + // Detects View's height + onLayout={(event) => { + const { height } = event.nativeEvent.layout; + setContentHeight(height); + }}> - {children} + {children}