Restore animation on channel list
This commit is contained in:
parent
8992f58512
commit
eb124f6210
5 changed files with 247 additions and 25 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
195
xtablo-expo/components/SwipeableChannelPreview.tsx
Normal file
195
xtablo-expo/components/SwipeableChannelPreview.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue