diff --git a/xtablo-expo/app/(app)/(tabs)/index.tsx b/xtablo-expo/app/(app)/(tabs)/index.tsx index 9438df2..127c17b 100644 --- a/xtablo-expo/app/(app)/(tabs)/index.tsx +++ b/xtablo-expo/app/(app)/(tabs)/index.tsx @@ -1,20 +1,21 @@ import { router } from "expo-router"; -import { ChannelList } from "stream-chat-expo"; +import { + ChannelList, + ChannelPreviewMessenger, + ChannelPreviewMessengerProps, + DefaultStreamChatGenerics, +} from "stream-chat-expo"; import { ChannelSort, Channel } from "stream-chat"; import { useUser } from "@/providers/UserProvider"; import { View, Text, StyleSheet, StatusBar } from "react-native"; import { LinearGradient } from "expo-linear-gradient"; import { Search } from "lucide-react-native"; import React, { useMemo } from "react"; -import { - useSharedValue, - useAnimatedStyle, - interpolate, - Extrapolate, -} from "react-native-reanimated"; +import { useSharedValue } from "react-native-reanimated"; import { useTablosList } from "@/hooks/tablos"; import { ColorMap } from "@/constants/colors"; import { UserTablo } from "@/types/tablos.types"; +import { SwipeableChannelPreview } from "@/components/SwipeableChannelPreview"; // Custom Avatar Component for Channel List @@ -29,10 +30,24 @@ const CustomChannelTitle = ({ channel }: { channel: Channel }) => { ); }; +// Custom Preview Component with Swipe to Archive +// Swipe left on any channel to reveal the archive button +const CustomChannelPreview = ( + props: ChannelPreviewMessengerProps +) => { + return ( + + + + ); +}; + export default function HomeScreen() { const user = useUser(); const { data: tablos, isLoading } = useTablosList(); + // const animations = useSharedValue>({}); + // Search animation state // const [isSearchVisible, setIsSearchVisible] = useState(false); // const [searchQuery, setSearchQuery] = useState(""); @@ -372,6 +387,17 @@ export default function HomeScreen() { }} sort={sort} options={options} + Preview={(props) => ( + { + // animations.value = { + // [id]: 0, + // }; + // }} + /> + )} PreviewAvatar={(props) => ( diff --git a/xtablo-expo/app/_layout.tsx b/xtablo-expo/app/_layout.tsx index d34e904..de44150 100644 --- a/xtablo-expo/app/_layout.tsx +++ b/xtablo-expo/app/_layout.tsx @@ -11,12 +11,10 @@ import { GestureHandlerRootView } from "react-native-gesture-handler"; import { useColorScheme } from "@/hooks/useColorScheme"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { cloneDeep } from "lodash"; -import { ActivityIndicator, View, StyleSheet, Image } from "react-native"; import { SplashScreenController } from "@/components/Splash"; import { useInitializeApp } from "@/hooks/auth"; -import { ThemedView } from "@/components/ThemedView"; -import { ThemedText } from "@/components/ThemedText"; import { LoadingView } from "@/components/LoadingView"; +import { ClickOutsideProvider } from "react-native-click-outside"; window.structuredClone = cloneDeep; @@ -39,15 +37,17 @@ export default function RootLayout() { return ( - - - - - - - + + + + + + + + + ); } diff --git a/xtablo-expo/components/SwipeableChannelPreview.tsx b/xtablo-expo/components/SwipeableChannelPreview.tsx new file mode 100644 index 0000000..102a135 --- /dev/null +++ b/xtablo-expo/components/SwipeableChannelPreview.tsx @@ -0,0 +1,195 @@ +import React from "react"; +import { View, Text, StyleSheet, Alert } from "react-native"; +import { + GestureDetector, + Gesture, + Pressable, +} from "react-native-gesture-handler"; +import Animated, { + useSharedValue, + useAnimatedStyle, + runOnJS, + withSpring, + interpolate, + Extrapolation, + FadeOut, + useDerivedValue, + SharedValue, +} from "react-native-reanimated"; +import { Archive } from "lucide-react-native"; +import { Channel } from "stream-chat"; +import { DefaultStreamChatGenerics } from "stream-chat-expo"; + +interface SwipeableChannelPreviewProps { + channel: Channel; + children: React.ReactNode; +} + +const SWIPE_THRESHOLD = -80; +const ACTION_WIDTH = 80; + +export const SwipeableChannelPreview: React.FC< + SwipeableChannelPreviewProps +> = ({ channel, children }) => { + const id = channel.id ?? ""; + const translateX = useSharedValue(0); + + const handleArchiveChannel = async () => { + try { + // Show confirmation dialog + Alert.alert( + "Archiver la conversation", + "Êtes-vous sûr de vouloir archiver cette conversation ainsi que le tablo associé ?", + [ + { + text: "Annuler", + style: "cancel", + onPress: () => { + // Close the swipe action + translateX.value = withSpring(0); + }, + }, + { + text: "Archiver", + style: "destructive", + onPress: async () => { + try { + // Hide the channel for the current user + await channel.hide(); + + // Close the swipe action + translateX.value = withSpring(0); + + // Show success message + Alert.alert( + "Succès", + "La conversation a été archivée avec succès", + [{ text: "OK" }] + ); + } catch (error) { + console.error("Error archiving channel:", error); + Alert.alert("Erreur", "Impossible d'archiver la conversation"); + } + }, + }, + ], + { cancelable: true } + ); + } catch (error) { + console.error("Error showing archive dialog:", error); + } + }; + + const gestureHandler = Gesture.Pan() + .onStart((context) => { + // cancelOtherAnimations(id); + context.translationX = translateX.value; + }) + .onUpdate((event) => { + // Only allow swiping left (negative values) + translateX.value = Math.min( + 0, + Math.max(SWIPE_THRESHOLD, event.translationX) + ); + }) + .onEnd((event) => { + const shouldOpen = translateX.value < SWIPE_THRESHOLD / 2; + + if (shouldOpen) { + translateX.value = withSpring(SWIPE_THRESHOLD); + } else { + translateX.value = withSpring(0); + } + }); + + const channelAnimatedStyle = useAnimatedStyle(() => { + return { + transform: [{ translateX: translateX.value }], + }; + }); + + const actionAnimatedStyle = useAnimatedStyle(() => { + const scale = interpolate( + translateX.value, + [SWIPE_THRESHOLD, 0], + [1, 0], + Extrapolation.CLAMP + ); + + const opacity = interpolate( + translateX.value, + [SWIPE_THRESHOLD, 0], + [1, 0], + Extrapolation.CLAMP + ); + + return { + transform: [{ scale }], + opacity, + }; + }); + + const onArchivePress = () => { + runOnJS(handleArchiveChannel)(); + }; + + return ( + + {/* Right Actions Background */} + + + + + Archiver + + + + + {/* Channel Content */} + + + {children} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + position: "relative", + }, + channelContainer: { + backgroundColor: "white", + flex: 1, + }, + rightActionsContainer: { + position: "absolute", + right: 0, + top: 0, + bottom: 0, + width: ACTION_WIDTH, + justifyContent: "center", + alignItems: "center", + }, + archiveButton: { + backgroundColor: "#166534", // Dark green color for archive + justifyContent: "center", + alignItems: "center", + width: ACTION_WIDTH, + height: "100%", + paddingHorizontal: 10, + }, + actionContent: { + justifyContent: "center", + alignItems: "center", + gap: 4, + }, + actionText: { + color: "white", + fontSize: 12, + fontWeight: "600", + textAlign: "center", + }, +}); diff --git a/xtablo-expo/package.json b/xtablo-expo/package.json index e4739c7..78ac640 100644 --- a/xtablo-expo/package.json +++ b/xtablo-expo/package.json @@ -25,13 +25,16 @@ "@tanstack/react-query": "^5.75.2", "aes-js": "^3.1.2", "expo": "^53.0.19", + "expo-auth-session": "~6.2.1", "expo-av": "~15.1.7", "expo-blur": "~14.1.5", "expo-constants": "~17.1.5", + "expo-crypto": "~14.1.5", "expo-font": "~13.3.2", "expo-haptics": "~14.1.4", "expo-image-manipulator": "~13.1.7", "expo-image-picker": "~16.1.4", + "expo-linear-gradient": "~14.1.5", "expo-linking": "~7.1.4", "expo-router": "~5.1.3", "expo-secure-store": "~14.2.3", @@ -55,10 +58,7 @@ "react-native-web": "^0.20.0", "react-native-webview": "13.13.5", "stream-chat-expo": "^6.7.3", - "zustand": "^5.0.4", - "expo-crypto": "~14.1.5", - "expo-auth-session": "~6.2.1", - "expo-linear-gradient": "~14.1.5" + "zustand": "^5.0.4" }, "devDependencies": { "@babel/core": "^7.25.2",