mirror of
https://github.com/urosran/cally.git
synced 2025-07-15 09:45:20 +00:00
Notification batch updates and notifications page update
This commit is contained in:
@ -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
|
||||||
|
@ -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
37
hooks/firebase/useDeleteNotification.ts
Normal file
37
hooks/firebase/useDeleteNotification.ts
Normal 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]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
Reference in New Issue
Block a user