as-app/src/scenes/Notifications/Item.js
2026-01-12 18:27:57 +01:00

612 lines
19 KiB
JavaScript

import React, { useRef, useEffect } from "react";
import { View, Text, TouchableOpacity, Alert } from "react-native";
import actionOpenAlert from "~/notifications/actions/actionOpenAlert";
import actionOpenRelatives from "~/notifications/actions/actionOpenRelatives";
import network from "~/network";
import {
Swipeable,
LongPressGestureHandler,
State,
} from "react-native-gesture-handler";
import { createStyles, useTheme } from "~/theme";
import { useMutation } from "@apollo/client";
import { format, fr } from "date-fns";
import { Feather } from "@expo/vector-icons";
import { DELETE_NOTIFICATION, MARK_NOTIFICATION_AS_READ } from "./gql";
import {
notificationsActions,
getNotificationsState,
aggregatedMessagesActions,
getAggregatedMessagesState,
paramsActions,
} from "~/stores";
import {
getNotificationTypeText,
getNotificationMessageText,
getNotificationColor,
createNotificationHandlers,
} from "~/notifications/notificationTypes";
import { getNotificationContent } from "~/notifications/content";
import { VirtualNotificationTypes } from "~/notifications/virtualNotifications";
const NotificationItem = ({
notification,
registerSwipeableRef,
unregisterSwipeableRef,
closeOtherSwipeables,
}) => {
const theme = useTheme();
const styles = useItemStyles();
const swipeableRef = useRef(null);
const [deleteNotification] = useMutation(DELETE_NOTIFICATION, {
optimisticResponse: {
deleteOneNotification: {
__typename: "notification",
},
},
update: (cache) => {
// Remove the notification from all queries in the cache
cache.modify({
fields: {
selectManyNotification: (
existingNotifications = [],
{ readField },
) => {
return existingNotifications.filter(
(notificationRef) =>
readField("id", notificationRef) !== notification.id,
);
},
},
});
},
});
const [markAsRead] = useMutation(MARK_NOTIFICATION_AS_READ, {
optimisticResponse: {
updateOneNotification: {
__typename: "notification",
id: notification.id,
acknowledged: true,
},
},
update: (cache, { data }) => {
// Get the notification ID
const notificationId = notification.id;
// Update the notification in the cache by directly modifying the cached object
cache.modify({
id: cache.identify({
__typename: "notification",
id: notificationId,
}),
fields: {
acknowledged: () => true,
},
});
// Also update any lists containing this notification
cache.modify({
fields: {
selectManyNotification: (
existingNotifications = [],
{ readField },
) => {
// Re-sort the notifications to match the expected order
// (unacknowledged first, then by creation date)
return [...existingNotifications].sort((a, b) => {
const aAcknowledged =
readField("id", a) === notificationId
? true
: readField("acknowledged", a);
const bAcknowledged =
readField("id", b) === notificationId
? true
: readField("acknowledged", b);
// First sort by acknowledged status (unacknowledged first)
if (aAcknowledged !== bAcknowledged) {
return aAcknowledged ? 1 : -1;
}
// Then sort by id (higher/newer first)
return readField("id", b) - readField("id", a);
});
},
},
});
},
});
// Effect to register/unregister this swipeable reference
useEffect(() => {
// Register this swipeable ref when component mounts
registerSwipeableRef(swipeableRef);
// Unregister when component unmounts
return () => {
unregisterSwipeableRef();
};
}, [registerSwipeableRef, unregisterSwipeableRef]);
// Function to handle long press - same effect as first swipe
const handleLongPress = ({ nativeEvent }) => {
if (nativeEvent.state === State.ACTIVE && swipeableRef.current) {
// Close all other open swipeables
closeOtherSwipeables();
// Open the swipeable to show action buttons
swipeableRef.current.openRight();
}
};
const formatDate = (dateString) => {
try {
const date = new Date(dateString);
return format(date, "d MMM yyyy à HH:mm", { locale: fr });
} catch (e) {
return dateString;
}
};
// Determine style based on acknowledged status
const itemStyle = [
styles.notificationItem,
notification.acknowledged
? styles.acknowledgedItem
: styles.unacknowledgedItem,
];
// Handle notification press based on notification type
const handleNotificationPress = async () => {
try {
// Create notification handlers with the necessary actions
const { virtualNotificationHandlers, regularNotificationHandlers } =
createNotificationHandlers({
openAlert: actionOpenAlert,
openRelatives: actionOpenRelatives,
});
// Handle virtual notifications
if (notification.isVirtual) {
const handler = virtualNotificationHandlers[notification.type];
if (handler) {
await handler(notification);
return;
}
}
// Handle regular notifications
if (!notification.data) return;
try {
const parsedData = JSON.parse(notification.data);
const handler = regularNotificationHandlers[notification.type];
if (handler) {
await handler(parsedData);
}
// Mark as read after pressing
if (!notification.acknowledged) {
handleMarkAsRead();
}
} catch (e) {
console.error("Error parsing notification data:", e);
}
} catch (error) {
console.error("Error handling notification press:", error);
}
};
const performMarkAsRead = async () => {
if (notification.acknowledged) return; // Skip if already acknowledged
try {
await markAsRead({
variables: { notificationId: notification.id },
});
// Close the swipeable component after the action is complete
if (swipeableRef.current) {
swipeableRef.current.close();
}
} catch (error) {
console.error("Error marking notification as read:", error);
Alert.alert("Erreur", "Impossible de marquer la notification comme lue.");
// Close the swipeable component even in case of error
if (swipeableRef.current) {
swipeableRef.current.close();
}
}
};
const performDelete = async () => {
try {
await deleteNotification({
variables: { id: notification.id },
});
// Close the swipeable component after the action is complete
if (swipeableRef.current) {
swipeableRef.current.close();
}
} catch (error) {
console.error("Error deleting notification:", error);
Alert.alert("Erreur", "Impossible de supprimer la notification.");
// Close the swipeable component even in case of error
if (swipeableRef.current) {
swipeableRef.current.close();
}
}
};
const handleDelete = () => {
try {
if (notification.isVirtual) {
// For virtual notifications, remove them from the local store
if (notification.type === VirtualNotificationTypes.REGISTER_RELATIVES) {
// Instead of filtering, set hasRegisteredRelatives to true
// This will prevent the virtual notification from being added in the future
paramsActions.setHasRegisteredRelatives(true);
} else if (
notification.type === VirtualNotificationTypes.UNREAD_ALERT_MESSAGES
) {
// Get all messages from the aggregatedMessages store
const { messagesList } = getAggregatedMessagesState();
// Find all messages related to this alert that are unread
const alertMessages = messagesList.filter(
(message) =>
message.oneAlert &&
message.oneAlert.id === notification.alertId &&
!message.isRead,
);
if (alertMessages.length > 0) {
// Get all message IDs
const messageIds = alertMessages.map((message) => message.id);
// Add a new function to the aggregatedMessages store to handle this case
aggregatedMessagesActions.markMultipleMessagesAsRead(messageIds);
}
}
// Close the swipeable component
if (swipeableRef.current) {
swipeableRef.current.close();
}
} else {
// For regular notifications, use the mutation
performDelete();
}
} catch (error) {
console.error("Error deleting notification:", error);
Alert.alert("Erreur", "Impossible de supprimer la notification.");
}
};
const handleMarkAsRead = () => {
try {
performMarkAsRead();
} catch (error) {
console.error("Error marking notification as read:", error);
Alert.alert("Erreur", "Impossible de marquer la notification comme lue.");
}
};
const renderRightActions = (progress, dragX) => {
return (
<View style={styles.actionsContainer}>
<View style={styles.buttonsWrapper}>
{/* Mark as Read button - only show if not acknowledged and not a virtual notification */}
{!notification.acknowledged && !notification.isVirtual && (
<TouchableOpacity
accessibilityRole="button"
onPress={handleMarkAsRead}
style={[styles.actionButton]}
activeOpacity={0.6}
>
<Feather
name="check-circle"
size={22}
color={theme.colors.primary}
/>
</TouchableOpacity>
)}
{/* Delete button */}
<TouchableOpacity
accessibilityRole="button"
onPress={handleDelete}
style={[
styles.actionButton,
// For virtual notifications, center the delete button vertically
notification.isVirtual && { height: "100%" },
]}
activeOpacity={0.6}
>
<Feather name="trash-2" size={22} color={theme.colors.error} />
</TouchableOpacity>
</View>
</View>
);
};
return (
<LongPressGestureHandler
onHandlerStateChange={handleLongPress}
minDurationMs={500}
>
<View>
<Swipeable
ref={swipeableRef}
friction={1}
rightThreshold={40}
overshootRight={true}
renderRightActions={renderRightActions}
onSwipeableWillOpen={() => {
// Close all other swipeables when this one opens
closeOtherSwipeables();
}}
>
<TouchableOpacity
accessibilityRole="button"
style={itemStyle}
onPress={handleNotificationPress}
activeOpacity={0.7}
>
<View style={styles.notificationHeader}>
<Text style={styles.notificationType}>
{getNotificationTypeText(notification)}
</Text>
{notification.acknowledged ? (
<Text style={styles.acknowledgedBadge}>Vu</Text>
) : (
<Text style={styles.unacknowledgedBadge}>Nouveau</Text>
)}
</View>
<Text style={styles.notificationMessage}>
{getNotificationMessageText(notification)}
</Text>
<View style={styles.notificationFooter}>
<View style={styles.footerLeft}>
{/* Display subject and alert code for virtual notifications */}
{notification.isVirtual &&
notification.type ===
VirtualNotificationTypes.UNREAD_ALERT_MESSAGES && (
<View style={styles.alertInfoContainer}>
{notification.alertSubject && (
<Text style={styles.alertSubject}>
{notification.alertSubject.length > 30
? notification.alertSubject.substring(0, 27) + "..."
: notification.alertSubject}
</Text>
)}
{notification.alertCode && (
<View style={styles.alertCodeContainer}>
{getNotificationColor(notification, theme) && (
<View
style={[
styles.colorDot,
{
backgroundColor: getNotificationColor(
notification,
theme,
),
},
]}
/>
)}
<Text style={styles.alertCode}>
#{notification.alertCode}
</Text>
</View>
)}
</View>
)}
{/* Display alert code for non-virtual notifications */}
{!notification.isVirtual &&
notification.data &&
(() => {
try {
const parsedData = JSON.parse(notification.data);
const content = getNotificationContent(
notification.type,
parsedData,
);
if (!content.code) return null;
return (
<View style={styles.alertCodeContainer}>
{getNotificationColor(
{ ...notification, data: parsedData },
theme,
) && (
<View
style={[
styles.colorDot,
{
backgroundColor: getNotificationColor(
{ ...notification, data: parsedData },
theme,
),
},
]}
/>
)}
<Text style={styles.alertCode}>#{content.code}</Text>
</View>
);
} catch (e) {
return null;
}
})()}
</View>
<View style={styles.footerRight}>
{notification.createdAt &&
!(
notification.isVirtual &&
notification.type ===
VirtualNotificationTypes.REGISTER_RELATIVES
) && (
<Text style={styles.notificationDate}>
{formatDate(notification.createdAt)}
</Text>
)}
</View>
</View>
</TouchableOpacity>
</Swipeable>
</View>
</LongPressGestureHandler>
);
};
// Separate styles for the notification item
const useItemStyles = createStyles(({ theme }) => ({
notificationItem: {
backgroundColor: theme.colors.surface,
borderRadius: 8,
padding: 15,
marginBottom: 10, // Keep this for spacing between cards
shadowColor: theme.colors.shadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 1.41,
elevation: 2,
},
acknowledgedItem: {
backgroundColor: theme.colors.surfaceVariant,
borderLeftWidth: 0,
opacity: 0.85,
},
unacknowledgedItem: {
backgroundColor: theme.colors.surface,
// Remove the left border as requested
},
notificationHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start", // Changed from center to allow wrapping
marginBottom: 8,
},
acknowledgedBadge: {
fontSize: 12,
color: theme.colors.onSurfaceVariant,
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
backgroundColor: theme.colors.surfaceVariant,
},
unacknowledgedBadge: {
fontSize: 12,
color: theme.colors.onPrimary,
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 10,
backgroundColor: theme.colors.primary,
fontWeight: "500",
},
notificationMessage: {
fontSize: 16,
color: theme.colors.onSurface,
marginBottom: 8,
},
titleContainer: {
flexDirection: "row",
alignItems: "center",
flex: 1,
},
colorDot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
notificationType: {
fontSize: 14,
color: theme.colors.onSurfaceVariant,
flex: 1, // Allow the text to take available space
flexWrap: "wrap", // Enable text wrapping
marginRight: 8, // Add some space between the text and the badge
},
notificationFooter: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginTop: 5,
},
footerLeft: {
flex: 1,
alignItems: "flex-start",
},
footerRight: {
flex: 1,
alignItems: "flex-end",
},
alertInfoContainer: {
flexDirection: "column",
alignItems: "flex-start",
},
alertSubject: {
fontSize: 12,
color: theme.colors.onSurface,
fontWeight: "500",
marginBottom: 4,
},
alertCodeContainer: {
flexDirection: "row",
alignItems: "center",
},
alertCode: {
fontSize: 12,
color: theme.colors.onSurfaceVariant,
fontWeight: "500",
},
notificationDate: {
fontSize: 12,
color: theme.colors.onSurfaceVariant,
textAlign: "right",
},
actionsContainer: {
marginLeft: 4, // Add small gap for visual separation
marginBottom: 10, // Match the card's marginBottom
width: 60, // Keep fixed width for icon-only buttons
alignSelf: "stretch", // Stretch to match the height of the card
},
buttonsWrapper: {
flexDirection: "column",
height: "100%", // Use full height
justifyContent: "space-between", // Distribute buttons equally
paddingVertical: 4, // Add some padding at top and bottom
},
actionButton: {
alignItems: "center",
justifyContent: "center",
borderRadius: 8, // Make all corners rounded
padding: 0, // Remove padding
height: "45%", // Take up almost half the available height
width: "100%", // Use full width of container
},
markAsReadButton: {
backgroundColor: theme.colors.surfaceVariant,
},
deleteButton: {
backgroundColor: theme.colors.surfaceVariant,
},
swipeActiveButton: {
backgroundColor: theme.custom.notifications.swipeActiveBackground,
},
deleteButtonSecondSwipe: {
backgroundColor: theme.custom.notifications.deleteSwipeBackground,
},
}));
export default NotificationItem;