Notification batch updates and notifications page update

This commit is contained in:
Milan Paunovic
2024-11-23 16:37:25 +01:00
parent cd178b8a9d
commit ee749e1077
5 changed files with 723 additions and 488 deletions

View File

@ -1,5 +1,5 @@
import {FlatList, StyleSheet} from "react-native"; import {ActivityIndicator, Animated, FlatList, StyleSheet} from "react-native";
import React, {useCallback} from "react"; import React, {useCallback, useState} from "react";
import {Card, Text, View} from "react-native-ui-lib"; import {Card, Text, View} from "react-native-ui-lib";
import HeaderTemplate from "@/components/shared/HeaderTemplate"; import HeaderTemplate from "@/components/shared/HeaderTemplate";
import {Notification, useGetNotifications} from "@/hooks/firebase/useGetNotifications"; import {Notification, useGetNotifications} from "@/hooks/firebase/useGetNotifications";
@ -7,12 +7,88 @@ import {formatDistanceToNow} from "date-fns";
import {useRouter} from "expo-router"; import {useRouter} from "expo-router";
import {useSetAtom} from "jotai"; import {useSetAtom} from "jotai";
import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms"; import {modeAtom, selectedDateAtom} from "@/components/pages/calendar/atoms";
import {Swipeable} from 'react-native-gesture-handler';
import {useDeleteNotification} from "@/hooks/firebase/useDeleteNotification";
const NotificationsPage = () => { interface NotificationItemProps {
item: Notification;
onDelete: (id: string) => void;
onPress: () => void;
isDeleting: boolean;
}
const NotificationItem: React.FC<NotificationItemProps> = React.memo(({
item,
onDelete,
onPress,
isDeleting
}) => {
const renderRightActions = useCallback((
progress: Animated.AnimatedInterpolation<number>,
dragX: Animated.AnimatedInterpolation<number>
) => {
const trans = dragX.interpolate({
inputRange: [-100, 0],
outputRange: [0, 100],
extrapolate: 'clamp'
});
return (
<Animated.View
style={[
styles.deleteAction,
{
transform: [{translateX: trans}],
},
]}
>
<Text style={styles.deleteActionText}>Delete</Text>
</Animated.View>
);
}, []);
return (
<Swipeable
renderRightActions={renderRightActions}
onSwipeableRightOpen={() => onDelete(item.id)}
overshootRight={false}
enabled={!isDeleting}
>
<Card
padding-20
marginB-10
onPress={onPress}
activeOpacity={0.6}
enableShadow={false}
style={styles.card}
>
{isDeleting && (
<View style={styles.loadingOverlay}>
<ActivityIndicator color="#000" size="large"/>
</View>
)}
<Text text70>{item.content}</Text>
<View row spread marginT-10>
<Text text90>
{formatDistanceToNow(new Date(item.timestamp), {addSuffix: true})}
</Text>
<Text text90>
{item.timestamp.toLocaleDateString()}
</Text>
</View>
</Card>
</Swipeable>
);
});
const NotificationsPage: React.FC = () => {
const setSelectedDate = useSetAtom(selectedDateAtom); const setSelectedDate = useSetAtom(selectedDateAtom);
const setMode = useSetAtom(modeAtom); const setMode = useSetAtom(modeAtom);
const {data: notifications} = useGetNotifications(); const {data: notifications} = useGetNotifications();
const deleteNotification = useDeleteNotification();
const {push} = useRouter(); const {push} = useRouter();
const [deletingIds, setDeletingIds] = useState<Set<string>>(new Set());
const goToEventDay = useCallback((notification: Notification) => () => { const goToEventDay = useCallback((notification: Notification) => () => {
if (notification?.date) { if (notification?.date) {
setSelectedDate(notification.date); setSelectedDate(notification.date);
@ -21,6 +97,27 @@ const NotificationsPage = () => {
push({pathname: "/calendar"}); push({pathname: "/calendar"});
}, [push, setSelectedDate]); }, [push, setSelectedDate]);
const handleDelete = useCallback((notificationId: string) => {
setDeletingIds(prev => new Set(prev).add(notificationId));
deleteNotification.mutate(notificationId, {
onSettled: () => {
setDeletingIds(prev => {
const newSet = new Set(prev);
newSet.delete(notificationId);
return newSet;
});
}
});
}, [deleteNotification]);
const renderNotificationItem = useCallback(({item}: { item: Notification }) => (
<NotificationItem
item={item}
onDelete={handleDelete}
onPress={goToEventDay(item)}
isDeleting={deletingIds.has(item.id)}
/>
), [handleDelete, goToEventDay, deletingIds]);
return ( return (
<View flexG height={"100%"}> <View flexG height={"100%"}>
@ -39,27 +136,8 @@ const NotificationsPage = () => {
<FlatList <FlatList
contentContainerStyle={styles.listContainer} contentContainerStyle={styles.listContainer}
data={notifications ?? []} data={notifications ?? []}
renderItem={({item}) => ( renderItem={renderNotificationItem}
<Card keyExtractor={(item) => item.id}
padding-20
marginB-10
key={item.content}
onPress={goToEventDay(item)}
activeOpacity={0.6}
enableShadow={false}
style={styles.card}
>
<Text text70>{item.content}</Text>
<View row spread marginT-10>
<Text text90>
{formatDistanceToNow(new Date(item.timestamp), {addSuffix: true})}
</Text>
<Text text90>
{item.timestamp.toLocaleDateString()}
</Text>
</View>
</Card>
)}
/> />
</View> </View>
</View> </View>
@ -79,14 +157,26 @@ const styles = StyleSheet.create({
fontFamily: "Manrope_400Regular", fontFamily: "Manrope_400Regular",
fontSize: 14, fontSize: 14,
}, },
searchField: { deleteAction: {
borderWidth: 0.7, backgroundColor: '#FF3B30',
borderColor: "#9b9b9b", justifyContent: 'center',
borderRadius: 15, alignItems: 'flex-end',
height: 42, paddingRight: 30,
paddingLeft: 10, marginBottom: 10,
marginVertical: 20, width: 100,
borderRadius: 10,
},
deleteActionText: {
color: 'white',
fontWeight: '600',
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(255, 255, 255, 0.8)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1,
}, },
}); });
export default NotificationsPage; export default NotificationsPage

View File

@ -164,19 +164,15 @@ export const AuthContextProvider: FC<{ children: ReactNode }> = ({children}) =>
} }
}, [user, ready, redirectOverride]); }, [user, ready, redirectOverride]);
// useEffect(() => { useEffect(() => {
// const handleNotification = async (notification: Notifications.Notification) => { const handleNotification = async (notification: Notifications.Notification) => {
// const eventId = notification?.request?.content?.data?.eventId; queryClient.invalidateQueries(["notifications"]);
// };
// // if (eventId) {
// queryClient.invalidateQueries(['events']); const sub = Notifications.addNotificationReceivedListener(handleNotification);
// // }
// }; return () => sub.remove();
// }, []);
// const sub = Notifications.addNotificationReceivedListener(handleNotification);
//
// return () => sub.remove();
// }, []);
if (!ready) { if (!ready) {
return null; return null;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
import {useMutation, useQueryClient} from "react-query";
import {useAuthContext} from "@/contexts/AuthContext";
import firestore from "@react-native-firebase/firestore";
import {Notification} from "@/hooks/firebase/useGetNotifications";
export const useDeleteNotification = () => {
const queryClient = useQueryClient();
const {user} = useAuthContext();
return useMutation({
mutationFn: async (id: string) => {
await firestore()
.collection("Notifications")
.doc(id)
.delete();
},
onMutate: async (deletedId) => {
await queryClient.cancelQueries(["notifications", user?.uid]);
const previousNotifications = queryClient.getQueryData<Notification[]>(["notifications", user?.uid]);
queryClient.setQueryData<Notification[]>(["notifications", user?.uid], (old) =>
old?.filter((notification) => notification?.id! !== deletedId) ?? []
);
return {previousNotifications};
},
onError: (_err, _deletedId, context) => {
if (context?.previousNotifications) {
queryClient.setQueryData(["notifications", user?.uid], context.previousNotifications);
}
},
onSettled: () => {
queryClient.invalidateQueries(["notifications", user?.uid]);
},
});
};

View File

@ -17,6 +17,7 @@ interface NotificationFirestore {
} }
export interface Notification { export interface Notification {
id: string;
creatorId: string; creatorId: string;
familyId: string; familyId: string;
content: string; content: string;
@ -40,11 +41,14 @@ export const useGetNotifications = () => {
const data = doc.data() as NotificationFirestore; const data = doc.data() as NotificationFirestore;
return { return {
id: doc.id,
...data, ...data,
timestamp: new Date(data.timestamp.seconds * 1000 + data.timestamp.nanoseconds / 1e6), timestamp: new Date(data.timestamp.seconds * 1000 + data.timestamp.nanoseconds / 1e6),
date: data.date ? new Date(data.date.seconds * 1000 + data.date.nanoseconds / 1e6) : undefined date: data.date ? new Date(data.date.seconds * 1000 + data.date.nanoseconds / 1e6) : undefined
}; };
}); });
} },
refetchOnWindowFocus: true,
staleTime: 60000,
}); });
}; };