Restore animation on channel list

This commit is contained in:
Arthur Belleville 2025-07-20 18:37:58 +02:00
parent 8992f58512
commit eb124f6210
No known key found for this signature in database
5 changed files with 247 additions and 25 deletions

View file

@ -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<DefaultStreamChatGenerics>
) => {
return (
<SwipeableChannelPreview channel={props.channel}>
<ChannelPreviewMessenger {...props} />
</SwipeableChannelPreview>
);
};
export default function HomeScreen() {
const user = useUser();
const { data: tablos, isLoading } = useTablosList();
// const animations = useSharedValue<Record<string, number>>({});
// 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) => (
<CustomChannelPreview
{...props}
// animations={animations}
// cancelOtherAnimations={(id) => {
// animations.value = {
// [id]: 0,
// };
// }}
/>
)}
PreviewAvatar={(props) => (
<CustomChannelAvatar
channel={props.channel}

View file

@ -445,8 +445,9 @@ export default function PlanningScreen() {
style={[
styles.tabloColorDot,
{
backgroundColor:
ColorMap[selectedTablo?.color ?? "bg-gray-500"],
backgroundColor: selectedTablo?.color
? ColorMap[selectedTablo.color]
: "#6b7280",
},
]}
/>

View file

@ -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 (
<GestureHandlerRootView style={{ flex: 1 }}>
<QueryClientProvider client={queryClient}>
<ThemeProvider
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
>
<SplashScreenController />
<RootNavigator />
<StatusBar style="auto" />
</ThemeProvider>
</QueryClientProvider>
<ClickOutsideProvider>
<QueryClientProvider client={queryClient}>
<ThemeProvider
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
>
<SplashScreenController />
<RootNavigator />
<StatusBar style="auto" />
</ThemeProvider>
</QueryClientProvider>
</ClickOutsideProvider>
</GestureHandlerRootView>
);
}

View file

@ -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<DefaultStreamChatGenerics>;
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 (
<View style={styles.container}>
{/* Right Actions Background */}
<View style={styles.rightActionsContainer}>
<Pressable style={styles.archiveButton} onPress={onArchivePress}>
<Animated.View style={[styles.actionContent, actionAnimatedStyle]}>
<Archive size={24} color="white" />
<Text style={styles.actionText}>Archiver</Text>
</Animated.View>
</Pressable>
</View>
{/* Channel Content */}
<GestureDetector gesture={gestureHandler}>
<Animated.View style={[styles.channelContainer, channelAnimatedStyle]}>
{children}
</Animated.View>
</GestureDetector>
</View>
);
};
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",
},
});

View file

@ -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",