added notification pagination

main
alex 2023-09-03 21:13:22 +02:00
parent 1e1e08bdda
commit b1ed49611c
8 changed files with 215 additions and 64 deletions

View File

@ -402,5 +402,16 @@
}, },
"noNotifications": "Keine Benachrichtigungen" "noNotifications": "Keine Benachrichtigungen"
} }
},
"liveTimeAgo": {
"justNow": "Gerade eben",
"day": "Vor {{count}} Tag",
"day_plural": "Vor {{count}} Tage",
"hour": "Vor {{count}} Stunde",
"hour_plural": "Vor {{count}} Stunden",
"minute": "Vor {{count}} Minute",
"minute_plural": "Vor {{count}} Minuten",
"second": "Vor {{count}} Sekunde",
"second_plural": "Vor {{count}} Sekunden"
} }
} }

View File

@ -401,5 +401,16 @@
}, },
"noNotifications": "No notifications" "noNotifications": "No notifications"
} }
},
"liveTimeAgo": {
"justNow": "Just now",
"day": "{{count}} day ago",
"day_plural": "{{count}} days ago",
"hour": "{{count}} hour ago",
"hour_plural": "{{count}} hours ago",
"minute": "{{count}} minute ago",
"minute_plural": "{{count}} minutes ago",
"second": "{{count}} second ago",
"second_plural": "{{count}} seconds ago"
} }
} }

View File

@ -1,7 +1,6 @@
import { import {
BellOutlined, BellOutlined,
CheckCircleOutlined, CheckCircleOutlined,
CheckOutlined,
CloseCircleOutlined, CloseCircleOutlined,
CloseOutlined, CloseOutlined,
DeleteOutlined, DeleteOutlined,
@ -16,10 +15,12 @@ import { Badge, Button, Drawer, List, Popconfirm, Typography } from "antd";
import { Header } from "antd/es/layout/layout"; import { Header } from "antd/es/layout/layout";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useHeaderContext } from "../../Contexts/HeaderContext"; import { useHeaderContext } from "../../Contexts/HeaderContext";
import { FormatDatetime, myFetch } from "../../utils"; import { myFetch } from "../../utils";
import { useWebSocketContext } from "../../Contexts/WebSocketContext"; import { useWebSocketContext } from "../../Contexts/WebSocketContext";
import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler"; import { SentMessagesCommands } from "../../Handlers/WebSocketMessageHandler";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LiveTimeAgo from "../LiveTimeAgo";
import MyPagination from "../MyPagination";
export default function HeaderMenu({ export default function HeaderMenu({
isSideMenuCollapsed, isSideMenuCollapsed,
@ -31,17 +32,35 @@ export default function HeaderMenu({
const [isNotificationDrawerOpen, setIsNotificationDrawerOpen] = const [isNotificationDrawerOpen, setIsNotificationDrawerOpen] =
useState(false); useState(false);
const fetchNotifications = (page = 1) => {
myFetch(`/notifications?page=${page}`, "GET").then((data) => {
console.log("data", data);
headerContext.setNotificationResponse(data);
});
};
const onPaginationChange = (page) => {
headerContext.setPaginationPage(page);
headerContext.paginationPageRef.current = page;
};
useEffect(() => { useEffect(() => {
// fetch will only be called if the drawer is open and there are no notifications // fetch will only be called if the drawer is open and there are no notifications
// further notifications will be fetched by the websocket // further notifications will be fetched by the websocket
if (!isNotificationDrawerOpen || headerContext.notifications !== null) if (!isNotificationDrawerOpen || headerContext.notficationResponse !== null)
return; return;
myFetch("/notifications", "GET").then((data) => fetchNotifications(1);
headerContext.setNotifications(data.Notifications)
);
}, [isNotificationDrawerOpen]); }, [isNotificationDrawerOpen]);
useEffect(() => {
if (!isNotificationDrawerOpen) return;
console.log("paginationPage", headerContext.paginationPage);
fetchNotifications(headerContext.paginationPage);
}, [headerContext.paginationPage]);
return ( return (
<Header <Header
style={{ style={{
@ -105,41 +124,53 @@ export default function HeaderMenu({
) )
} }
> >
{headerContext.totalNotifications === 0 || {isNotificationDrawerOpen && (
headerContext.notifications === null ? (
<div style={{ textAlign: "center" }}>
<InboxOutlined style={{ fontSize: 32, marginBottom: 10 }} />
<Typography.Title level={5}>
{t("header.notificationDrawer.noNotifications")}
</Typography.Title>
</div>
) : (
<> <>
<List {headerContext.totalNotifications === 0 ||
dataSource={headerContext.notifications.sort((a, b) => { headerContext.notficationResponse === null ? (
return new Date(b.CreatedAt) - new Date(a.CreatedAt); <div style={{ textAlign: "center" }}>
})} <InboxOutlined style={{ fontSize: 32, marginBottom: 10 }} />
renderItem={(item) => ( <Typography.Title level={5}>
<List.Item> {t("header.notificationDrawer.noNotifications")}
<List.Item.Meta </Typography.Title>
avatar={<NotificationTypeIcon type={item.Type} />} </div>
title={item.Title} ) : (
description={FormatDatetime(item.CreatedAt)} <List
dataSource={headerContext.notficationResponse.Notifications.sort(
(a, b) => {
return new Date(b.CreatedAt) - new Date(a.CreatedAt);
}
)}
footer={
<MyPagination
paginationPage={headerContext.paginationPage}
setPaginationPage={(page) => onPaginationChange(page)}
totalPages={headerContext.notficationResponse.TotalPages}
size="small"
/> />
}
renderItem={(item) => (
<List.Item>
<List.Item.Meta
avatar={<NotificationTypeIcon type={item.Type} />}
title={item.Title}
description={<LiveTimeAgo startTime={item.CreatedAt} />}
/>
<CloseOutlined <CloseOutlined
onClick={() => onClick={() =>
webSocketContext.SendSocketMessage( webSocketContext.SendSocketMessage(
SentMessagesCommands.DeleteOneNotification, SentMessagesCommands.DeleteOneNotification,
{ {
notificationId: item.Id, notificationId: item.Id,
} }
) )
} }
/> />
</List.Item> </List.Item>
)} )}
/> />
)}
</> </>
)} )}
</Drawer> </Drawer>

View File

@ -0,0 +1,61 @@
import { Tooltip } from "antd";
import { useEffect, useState } from "react";
import { FormatDatetime } from "../../utils";
import { useTranslation } from "react-i18next";
// Calculate elapsed time in seconds, minutes, hours, or days
function calculatedTimeAgo(startTime, t) {
const currentTime = new Date();
const elapsedMilliseconds = currentTime - new Date(startTime);
let timeAgoText;
if (elapsedMilliseconds < 60000) {
const secondsAgo = Math.floor(elapsedMilliseconds / 1000);
timeAgoText =
secondsAgo === 0
? "just now"
: secondsAgo === 1
? t("liveTimeAgo.second", { count: secondsAgo })
: t("liveTimeAgo.second_plural", { count: secondsAgo });
} else if (elapsedMilliseconds < 3600000) {
const minutesAgo = Math.floor(elapsedMilliseconds / 60000);
timeAgoText =
minutesAgo === 1
? t("liveTimeAgo.minute", { count: minutesAgo })
: t("liveTimeAgo.minute_plural", { count: minutesAgo });
} else if (elapsedMilliseconds < 86400000) {
const hoursAgo = Math.floor(elapsedMilliseconds / 3600000);
timeAgoText =
hoursAgo === 1
? t("liveTimeAgo.hour", { count: hoursAgo })
: t("liveTimeAgo.hour_plural", { count: hoursAgo });
} else {
const daysAgo = Math.floor(elapsedMilliseconds / 86400000);
timeAgoText =
daysAgo === 1
? t("liveTimeAgo.day", { count: daysAgo })
: t("liveTimeAgo.day_plural", { count: daysAgo });
}
return timeAgoText;
}
export default function LiveTimeAgo({ startTime }) {
const { t } = useTranslation();
const [timeAgo, setTimeAgo] = useState(calculatedTimeAgo(startTime, t));
useEffect(() => {
const interval = setInterval(() => {
setTimeAgo(calculatedTimeAgo(startTime, t));
}, 1000); // Update every second
return () => clearInterval(interval); // Clean up on component unmount
}, [startTime]);
return (
<Tooltip title={FormatDatetime(startTime)}>
<span>{timeAgo}</span>
</Tooltip>
);
}

View File

@ -5,9 +5,11 @@ export default function MyPagination({
paginationPage, paginationPage,
setPaginationPage, setPaginationPage,
totalPages, totalPages,
size,
}) { }) {
return ( return (
<Pagination <Pagination
size={size}
style={{ marginTop: AppStyle.app.margin, textAlign: "right" }} style={{ marginTop: AppStyle.app.margin, textAlign: "right" }}
showSizeChanger={false} showSizeChanger={false}
current={paginationPage} current={paginationPage}

View File

@ -1,8 +1,10 @@
import { createContext, useContext, useState } from "react"; import { createContext, useContext, useRef, useState } from "react";
const preview = { const preview = {
totalNotifications: 0, totalNotifications: 0,
notifications: [], notficationResponse: null,
paginationPage: 1,
paginationPageRef: null,
}; };
const HeaderContext = createContext(preview); const HeaderContext = createContext(preview);
@ -11,16 +13,20 @@ export const useHeaderContext = () => useContext(HeaderContext);
export default function HeaderProvider({ children }) { export default function HeaderProvider({ children }) {
const [totalNotifications, setTotalNotifications] = useState(0); const [totalNotifications, setTotalNotifications] = useState(0);
// initially null, then set to [...] on first fetch const [notficationResponse, setNotificationResponse] = useState(null);
const [notifications, setNotifications] = useState(null); const [paginationPage, setPaginationPage] = useState(1);
const paginationPageRef = useRef(paginationPage);
return ( return (
<HeaderContext.Provider <HeaderContext.Provider
value={{ value={{
totalNotifications, totalNotifications,
setTotalNotifications, setTotalNotifications,
notifications, notficationResponse,
setNotifications, setNotificationResponse,
paginationPage,
setPaginationPage,
paginationPageRef,
}} }}
> >
{children} {children}

View File

@ -109,7 +109,9 @@ export function handleWebSocketMessage(
groupTasksContext.setGroupTasks((arr) => { groupTasksContext.setGroupTasks((arr) => {
const newArr = [...arr]; const newArr = [...arr];
if (newArr.length === 5) { if (
newArr.length === Constants.GLOBALS.GROUP_TASKS_PAGINATION_LIMIT
) {
newArr.pop(); newArr.pop();
} }
@ -969,40 +971,66 @@ export function handleWebSocketMessage(
}); });
break; break;
case ReceivedMessagesCommands.NewNotification: case ReceivedMessagesCommands.NewNotification:
if (headerContext.paginationPageRef.current === 1) {
console.log("new body", body);
headerContext.setNotificationResponse((obj) => {
if (obj === null) return obj;
const newArr = [...obj.Notifications];
if (
newArr.length === Constants.GLOBALS.NOTIFICATIONS_PAGINATION_LIMIT
) {
newArr.pop();
}
newArr.unshift(body.Notification);
obj.Notifications = newArr;
obj.TotalPages = body.TotalPages;
return obj;
});
}
headerContext.setTotalNotifications( headerContext.setTotalNotifications(
(totalNotifications) => totalNotifications + 1 (totalNotifications) => totalNotifications + 1
); );
headerContext.setNotifications((arr) => {
// only add notifications to the list if the list is not null
// this has to do with the get fetch that is executed when the list is empty
if (arr === null) return arr;
const newArr = [...arr];
newArr.push(body);
return newArr;
});
break; break;
case ReceivedMessagesCommands.AllNotificationsDeleted: case ReceivedMessagesCommands.AllNotificationsDeleted:
headerContext.setNotificationResponse(null);
headerContext.setTotalNotifications(0); headerContext.setTotalNotifications(0);
headerContext.setPaginationPage(1);
headerContext.setNotifications([]);
break; break;
case ReceivedMessagesCommands.OneNotificationDeleted: case ReceivedMessagesCommands.OneNotificationDeleted:
headerContext.setTotalNotifications( headerContext.setTotalNotifications(
(totalNotifications) => totalNotifications - 1 (totalNotifications) => totalNotifications - 1
); );
headerContext.setNotifications((arr) => { headerContext.setNotificationResponse((obj) => {
if (arr === null) return arr; if (obj === null) return obj;
let newArr = [...arr]; const newArr = [...obj.Notifications];
newArr = newArr.filter((notification) => notification.Id !== body); const arrIndex = newArr.findIndex(
(notification) => notification.Id === body
);
return newArr; if (arrIndex !== -1) {
newArr.splice(arrIndex, 1);
}
obj.Notifications = newArr;
if (newArr.length === 0) {
const newPaginationPage = headerContext.paginationPageRef.current - 1;
headerContext.setPaginationPage(newPaginationPage);
headerContext.paginationPageRef.current = newPaginationPage;
}
return obj;
}); });
break; break;
default: default:

View File

@ -76,6 +76,7 @@ export const Constants = {
MAX_EQUIPMENT_DOCUMENTATION_NOTE_LENGTH: 2000, MAX_EQUIPMENT_DOCUMENTATION_NOTE_LENGTH: 2000,
EQUIPMENT_DOCUMENTATIONS_PAGINATION_LIMIT: 3, EQUIPMENT_DOCUMENTATIONS_PAGINATION_LIMIT: 3,
GROUP_TASKS_PAGINATION_LIMIT: 5, GROUP_TASKS_PAGINATION_LIMIT: 5,
NOTIFICATIONS_PAGINATION_LIMIT: 10,
}, },
MAX_AVATAR_SIZE: 5 * 1024 * 1024, MAX_AVATAR_SIZE: 5 * 1024 * 1024,
ACCEPTED_AVATAR_FILE_TYPES: [ ACCEPTED_AVATAR_FILE_TYPES: [