Merged in release/2024-03-14 (pull request #2167)
IO-3166-Global-Notifications-Part-2 - Checkpoint
This commit is contained in:
@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./notification-center.styles.scss";
|
||||
import day from "../../utils/day.js";
|
||||
import { forwardRef } from "react";
|
||||
import { forwardRef, useRef, useEffect } from "react";
|
||||
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
@@ -32,6 +32,14 @@ const NotificationCenterComponent = forwardRef(
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const virtuosoRef = useRef(null);
|
||||
|
||||
// Scroll to top when showUnreadOnly changes
|
||||
useEffect(() => {
|
||||
if (virtuosoRef.current) {
|
||||
virtuosoRef.current.scrollToIndex({ index: 0, behavior: "smooth" });
|
||||
}
|
||||
}, [showUnreadOnly]);
|
||||
|
||||
const renderNotification = (index, notification) => {
|
||||
const handleClick = () => {
|
||||
@@ -99,6 +107,7 @@ const NotificationCenterComponent = forwardRef(
|
||||
</div>
|
||||
</div>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
style={{ height: "400px", width: "100%" }}
|
||||
data={notifications}
|
||||
totalCount={notifications.length}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; // Add useRef
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import NotificationCenterComponent from "./notification-center.component";
|
||||
@@ -23,8 +23,9 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||
const notificationRef = useRef(null); // Add ref for the notification center
|
||||
const notificationRef = useRef(null);
|
||||
|
||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||
|
||||
@@ -36,7 +37,12 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause;
|
||||
}, [baseWhereClause, showUnreadOnly]);
|
||||
|
||||
const { data, fetchMore, loading, refetch } = useQuery(GET_NOTIFICATIONS, {
|
||||
const {
|
||||
data,
|
||||
fetchMore,
|
||||
loading: queryLoading,
|
||||
refetch
|
||||
} = useQuery(GET_NOTIFICATIONS, {
|
||||
variables: {
|
||||
limit: INITIAL_NOTIFICATIONS,
|
||||
offset: 0,
|
||||
@@ -47,27 +53,21 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||
skip: !userAssociationId,
|
||||
onError: (err) => {
|
||||
console.error(`Error polling Notifications in notification-center: ${err?.message || ""}`);
|
||||
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||
}
|
||||
});
|
||||
|
||||
// Handle click outside to close
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
// Prevent open + close behavior from the header
|
||||
if (event.target.closest("#header-notifications")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.closest("#header-notifications")) return;
|
||||
if (visible && notificationRef.current && !notificationRef.current.contains(event.target)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [visible, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,7 +105,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
}, [data]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!loading && data?.notifications.length) {
|
||||
if (!queryLoading && data?.notifications.length) {
|
||||
setIsLoading(true); // Show spinner during fetchMore
|
||||
fetchMore({
|
||||
variables: { offset: data.notifications.length, where: whereClause },
|
||||
updateQuery: (prev, { fetchMoreResult }) => {
|
||||
@@ -114,58 +115,55 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
notifications: [...prev.notifications, ...fetchMoreResult.notifications]
|
||||
};
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error("Fetch more error:", err);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Fetch more error:", err);
|
||||
})
|
||||
.finally(() => setIsLoading(false)); // Hide spinner when done
|
||||
}
|
||||
}, [data?.notifications?.length, fetchMore, loading, whereClause]);
|
||||
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
|
||||
|
||||
const handleToggleUnreadOnly = (value) => {
|
||||
setShowUnreadOnly(value);
|
||||
};
|
||||
|
||||
const handleMarkAllRead = useCallback(() => {
|
||||
setIsLoading(true);
|
||||
markAllNotificationsRead()
|
||||
.then(() => {
|
||||
const timestamp = new Date().toISOString();
|
||||
setNotifications((prev) => {
|
||||
const updatedNotifications = prev.map((notif) =>
|
||||
notif.read === null && notif.associationid === userAssociationId
|
||||
? {
|
||||
...notif,
|
||||
read: timestamp
|
||||
}
|
||||
: notif
|
||||
);
|
||||
return [...updatedNotifications];
|
||||
});
|
||||
setNotifications((prev) =>
|
||||
prev.map((notif) =>
|
||||
notif.read === null && notif.associationid === userAssociationId ? { ...notif, read: timestamp } : notif
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`));
|
||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [markAllNotificationsRead, userAssociationId]);
|
||||
|
||||
const handleNotificationClick = useCallback(
|
||||
(notificationId) => {
|
||||
markNotificationRead({
|
||||
variables: { id: notificationId }
|
||||
})
|
||||
setIsLoading(true);
|
||||
markNotificationRead({ variables: { id: notificationId } })
|
||||
.then(() => {
|
||||
const timestamp = new Date().toISOString();
|
||||
setNotifications((prev) => {
|
||||
return prev.map((notif) =>
|
||||
notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif
|
||||
);
|
||||
});
|
||||
setNotifications((prev) =>
|
||||
prev.map((notif) => (notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif))
|
||||
);
|
||||
})
|
||||
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`));
|
||||
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`))
|
||||
.finally(() => setIsLoading(false));
|
||||
},
|
||||
[markNotificationRead]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && !isConnected) {
|
||||
refetch().catch(
|
||||
(err) => `Something went wrong re-fetching notifications in the notification-center: ${err?.message || ""}`
|
||||
);
|
||||
setIsLoading(true);
|
||||
refetch()
|
||||
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
}, [visible, isConnected, refetch]);
|
||||
|
||||
@@ -175,7 +173,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
notifications={notifications}
|
||||
loading={loading}
|
||||
loading={isLoading}
|
||||
showUnreadOnly={showUnreadOnly}
|
||||
toggleUnreadOnly={handleToggleUnreadOnly}
|
||||
markAllRead={handleMarkAllRead}
|
||||
|
||||
@@ -143,7 +143,41 @@ middlewares.push(
|
||||
new SentryLink().concat(roundTripLink.concat(retryLink.concat(errorLink.concat(authLink.concat(link)))))
|
||||
);
|
||||
|
||||
const cache = new InMemoryCache({});
|
||||
const cache = new InMemoryCache({
|
||||
typePolicies: {
|
||||
Query: {
|
||||
fields: {
|
||||
// Note: This is required because we switch from a read to an unread state with a toggle,
|
||||
notifications: {
|
||||
merge(existing = [], incoming = [], { readField }) {
|
||||
// Create a map to deduplicate by __ref
|
||||
const merged = new Map();
|
||||
|
||||
// Add existing items to retain cached data
|
||||
existing.forEach((item) => {
|
||||
const ref = readField("__ref", item);
|
||||
if (ref) {
|
||||
merged.set(ref, item);
|
||||
}
|
||||
});
|
||||
|
||||
// Add incoming items, overwriting duplicates
|
||||
incoming.forEach((item) => {
|
||||
const ref = readField("__ref", item);
|
||||
if (ref) {
|
||||
merged.set(ref, item);
|
||||
}
|
||||
});
|
||||
|
||||
// Return incoming to respect the current query’s filter (e.g., unread-only or all)
|
||||
return incoming;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const client = new ApolloClient({
|
||||
link: ApolloLink.from(middlewares),
|
||||
cache,
|
||||
@@ -163,4 +197,5 @@ const client = new ApolloClient({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default client;
|
||||
|
||||
Reference in New Issue
Block a user