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 { useNavigate } from "react-router-dom";
|
||||||
import "./notification-center.styles.scss";
|
import "./notification-center.styles.scss";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { forwardRef } from "react";
|
import { forwardRef, useRef, useEffect } from "react";
|
||||||
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -32,6 +32,14 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
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 renderNotification = (index, notification) => {
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
@@ -99,6 +107,7 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
|
ref={virtuosoRef}
|
||||||
style={{ height: "400px", width: "100%" }}
|
style={{ height: "400px", width: "100%" }}
|
||||||
data={notifications}
|
data={notifications}
|
||||||
totalCount={notifications.length}
|
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 { useQuery } from "@apollo/client";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import NotificationCenterComponent from "./notification-center.component";
|
import NotificationCenterComponent from "./notification-center.component";
|
||||||
@@ -23,8 +23,9 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
|||||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
||||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
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;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
|
||||||
@@ -36,7 +37,12 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause;
|
return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause;
|
||||||
}, [baseWhereClause, showUnreadOnly]);
|
}, [baseWhereClause, showUnreadOnly]);
|
||||||
|
|
||||||
const { data, fetchMore, loading, refetch } = useQuery(GET_NOTIFICATIONS, {
|
const {
|
||||||
|
data,
|
||||||
|
fetchMore,
|
||||||
|
loading: queryLoading,
|
||||||
|
refetch
|
||||||
|
} = useQuery(GET_NOTIFICATIONS, {
|
||||||
variables: {
|
variables: {
|
||||||
limit: INITIAL_NOTIFICATIONS,
|
limit: INITIAL_NOTIFICATIONS,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -47,27 +53,21 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||||
skip: !userAssociationId,
|
skip: !userAssociationId,
|
||||||
onError: (err) => {
|
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());
|
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle click outside to close
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
// Prevent open + close behavior from the header
|
// Prevent open + close behavior from the header
|
||||||
if (event.target.closest("#header-notifications")) {
|
if (event.target.closest("#header-notifications")) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (visible && notificationRef.current && !notificationRef.current.contains(event.target)) {
|
if (visible && notificationRef.current && !notificationRef.current.contains(event.target)) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
return () => {
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, [visible, onClose]);
|
}, [visible, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -105,7 +105,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (!loading && data?.notifications.length) {
|
if (!queryLoading && data?.notifications.length) {
|
||||||
|
setIsLoading(true); // Show spinner during fetchMore
|
||||||
fetchMore({
|
fetchMore({
|
||||||
variables: { offset: data.notifications.length, where: whereClause },
|
variables: { offset: data.notifications.length, where: whereClause },
|
||||||
updateQuery: (prev, { fetchMoreResult }) => {
|
updateQuery: (prev, { fetchMoreResult }) => {
|
||||||
@@ -114,58 +115,55 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
notifications: [...prev.notifications, ...fetchMoreResult.notifications]
|
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) => {
|
const handleToggleUnreadOnly = (value) => {
|
||||||
setShowUnreadOnly(value);
|
setShowUnreadOnly(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAllRead = useCallback(() => {
|
const handleMarkAllRead = useCallback(() => {
|
||||||
|
setIsLoading(true);
|
||||||
markAllNotificationsRead()
|
markAllNotificationsRead()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
setNotifications((prev) => {
|
setNotifications((prev) =>
|
||||||
const updatedNotifications = prev.map((notif) =>
|
prev.map((notif) =>
|
||||||
notif.read === null && notif.associationid === userAssociationId
|
notif.read === null && notif.associationid === userAssociationId ? { ...notif, read: timestamp } : notif
|
||||||
? {
|
)
|
||||||
...notif,
|
);
|
||||||
read: timestamp
|
|
||||||
}
|
|
||||||
: notif
|
|
||||||
);
|
|
||||||
return [...updatedNotifications];
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.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]);
|
}, [markAllNotificationsRead, userAssociationId]);
|
||||||
|
|
||||||
const handleNotificationClick = useCallback(
|
const handleNotificationClick = useCallback(
|
||||||
(notificationId) => {
|
(notificationId) => {
|
||||||
markNotificationRead({
|
setIsLoading(true);
|
||||||
variables: { id: notificationId }
|
markNotificationRead({ variables: { id: notificationId } })
|
||||||
})
|
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
setNotifications((prev) => {
|
setNotifications((prev) =>
|
||||||
return prev.map((notif) =>
|
prev.map((notif) => (notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : 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]
|
[markNotificationRead]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !isConnected) {
|
if (visible && !isConnected) {
|
||||||
refetch().catch(
|
setIsLoading(true);
|
||||||
(err) => `Something went wrong re-fetching notifications in the notification-center: ${err?.message || ""}`
|
refetch()
|
||||||
);
|
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
||||||
|
.finally(() => setIsLoading(false));
|
||||||
}
|
}
|
||||||
}, [visible, isConnected, refetch]);
|
}, [visible, isConnected, refetch]);
|
||||||
|
|
||||||
@@ -175,7 +173,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
visible={visible}
|
visible={visible}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
loading={loading}
|
loading={isLoading}
|
||||||
showUnreadOnly={showUnreadOnly}
|
showUnreadOnly={showUnreadOnly}
|
||||||
toggleUnreadOnly={handleToggleUnreadOnly}
|
toggleUnreadOnly={handleToggleUnreadOnly}
|
||||||
markAllRead={handleMarkAllRead}
|
markAllRead={handleMarkAllRead}
|
||||||
|
|||||||
@@ -143,7 +143,41 @@ middlewares.push(
|
|||||||
new SentryLink().concat(roundTripLink.concat(retryLink.concat(errorLink.concat(authLink.concat(link)))))
|
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({
|
const client = new ApolloClient({
|
||||||
link: ApolloLink.from(middlewares),
|
link: ApolloLink.from(middlewares),
|
||||||
cache,
|
cache,
|
||||||
@@ -163,4 +197,5 @@ const client = new ApolloClient({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default client;
|
export default client;
|
||||||
|
|||||||
Reference in New Issue
Block a user