frontend/src/app/NotificationDrawer.tsx

296 lines
11 KiB
TypeScript

import {
Avatar,
Button,
CircularProgress,
createStyles,
Divider,
Drawer,
Grid,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
Theme,
Tooltip,
Typography,
useTheme
} from "@mui/material";
import { makeStyles } from "@mui/styles"
import { Remove } from "@mui/icons-material";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import DeviceIcon from "assets/editor/device.png";
import { useMobile, useUserAPI } from "hooks";
import moment from "moment";
//import { GetComponentIcon } from "pbHelpers/ComponentType"; //TODO: Put this pack in for the icons once components are done
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { useNotificationAPI } from "providers";
import React, { useCallback, useEffect, useState } from "react";
import { useNavigate } from "react-router";
const useStyles = makeStyles((theme: Theme) => ({
chatDrawer: {
padding: theme.spacing(1),
height: "100%",
minWidth: theme.spacing(54),
maxWidth: theme.spacing(54)
},
chatDrawerMobile: {
padding: theme.spacing(1),
height: "auto",
width: "100%"
},
drawerHeader: {
display: "flex",
alignItems: "center",
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
justifyContent: "space-between"
}
}));
interface Props {
notificationDrawerOpen: boolean;
setNotificationDrawerOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
export default function NotificationDrawer(props: Props) {
const { notificationDrawerOpen, setNotificationDrawerOpen } = props;
const [loading, setLoading] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const [notifications, setNotifications] = useState<Map<string, pond.Notification>>(new Map());
const [offset, setOffset] = useState(0);
const [total, setTotal] = useState(9999999);
const history = useNavigate();
const isMobile = useMobile();
const classes = useStyles();
const theme = useTheme();
const notificationAPI = useNotificationAPI();
const userAPI = useUserAPI();
const [{ user, as }] = useGlobalState();
const loadNotifications = useCallback(() => {
if (user.id().length < 1) return;
setLoading(true);
notificationAPI
.listNotifications(10, 0, "desc", "timestamp", undefined, as)
.then(resp => {
let r = pond.ListNotificationsResponse.fromObject(resp.data);
setTotal(r.total);
let n: Map<string, pond.Notification> = new Map();
r.notifications.forEach(noti => {
n.set(noti.settings?.key ? noti.settings.key : "", noti);
});
setNotifications(n);
let u = user.protobuf();
if (u.status) {
let DATE_RFC2822 = "ddd, DD MMM YYYY HH:mm:ss ZZ";
u.status.lastNotificationViewed = moment()
.locale("en")
.format(DATE_RFC2822);
userAPI.updateUser(user.id(), u);
}
})
.finally(() => {
setLoading(false);
});
}, [notificationAPI, setLoading, setNotifications, user, userAPI, as]);
useEffect(() => {
if (offset === 0) return;
setLoadingMore(true);
notificationAPI
.listNotifications(10, offset, "desc", "timestamp", undefined, as)
.then(resp => {
let r = pond.ListNotificationsResponse.fromObject(resp.data);
let n: Map<string, pond.Notification> = new Map(notifications);
r.notifications.forEach(noti => {
n.set(noti.settings?.key ? noti.settings.key : "", noti);
});
setNotifications(n);
})
.finally(() => {
setLoadingMore(false);
});
}, [offset, notificationAPI]); // eslint-disable-line react-hooks/exhaustive-deps
const removeNotification = (keys: string[]) => {
notificationAPI.hideNotification(keys).then(() => {
let n = new Map(notifications);
keys.forEach(key => {
n.delete(key);
});
setNotifications(n);
setTotal(total - keys.length);
});
};
const hasSource = (notification: pond.Notification) => {
return Boolean(notification.settings?.sourceType && notification.settings?.sourceKey);
};
const notificationClick = (notification: pond.Notification) => {
setNotificationDrawerOpen(false);
setNotifications(new Map());
if (!notification.settings?.sourceType) return;
if (!notification.settings?.sourceKey) return;
let url = "/" + notification.settings?.sourceType + "s/" + notification.settings?.sourceKey;
history(url); //in order to update the history stack add '{ replace: true }' as a second parameter
};
useEffect(() => {
if (loading) return;
if (!notificationDrawerOpen) return;
loadNotifications();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadNotifications, notificationDrawerOpen]);
return (
<Drawer
anchor="right"
open={notificationDrawerOpen}
onClose={() => setNotificationDrawerOpen(false)}
style={{ height: "100%", maxWidth: "100%" }}
title={"Notifications"}>
<div className={isMobile ? classes.chatDrawerMobile : classes.chatDrawer}>
<div className={classes.drawerHeader}>
<IconButton onClick={() => setNotificationDrawerOpen(false)}>
<ChevronRightIcon />
</IconButton>
<Typography style={{ fontWeight: 650, fontSize: 20 }}>Notifications</Typography>
<Button
variant="contained"
color="primary"
onClick={() => {
removeNotification(Array.from(notifications.keys()));
}}>
Clear
</Button>
</div>
<List>
<Divider />
{loading && (
<div style={{ textAlign: "center", margin: theme.spacing(2) }}>
<CircularProgress />
</div>
)}
{!loading && notifications.size < 1 && (
<Typography
color="textSecondary"
style={{ textAlign: "center", margin: theme.spacing(2) }}>
No Notifications
</Typography>
)}
{!loading &&
[...notifications.values()].map(notification => {
let cIcon = DeviceIcon;
// TODO: put this back in when the component stuff has been added
// if (notification.settings?.componentSource) {
// let newIcon = GetComponentIcon(
// notification.settings.componentSource.type,
// undefined,
// theme.palette.type
// );
// cIcon = newIcon ? newIcon : cIcon;
// } else if (notification.settings?.logo) {
// cIcon = notification.settings?.logo;
// }
let title = notification.settings?.title
? notification.settings?.title
: "Device " + notification.settings?.sourceKey;
//notification.settings?.sourceKey
return (
<React.Fragment key={notification.settings?.key}>
<ListItem
// button={hasSource(notification) as any}
onClick={() => notificationClick(notification)}
>
<ListItemIcon>
{/*<ImgIcon src={cIcon} />*/}
<Avatar alt={user.name()} src={cIcon} />
</ListItemIcon>
<ListItemText>
<Grid container direction="column">
<Grid container direction="row" alignItems="center" alignContent="center">
<Grid item>
<Typography>{title}</Typography>
</Grid>
<Grid item>
<Typography
color={"textSecondary"}
style={{ marginLeft: theme.spacing(0.5) }}>
{notification.settings?.subtitle &&
" - " + notification.settings?.subtitle}
</Typography>
</Grid>
</Grid>
{notification.status?.timestamp && (
<Grid
item
style={{
marginTop: theme.spacing(-0.75),
marginLeft: theme.spacing(1)
}}>
<Typography variant="caption" color="textSecondary">
{moment(notification.status.timestamp).fromNow()}
</Typography>
</Grid>
)}
{hasSource(notification) && (
<Grid item>
<Typography variant="subtitle1" color="textSecondary">
Click to view {notification.settings?.sourceType}
</Typography>
</Grid>
)}
</Grid>
</ListItemText>
<ListItemSecondaryAction>
<Tooltip title="Hide Notification">
<IconButton
onClick={() =>
removeNotification(
notification.settings?.key ? [notification.settings.key] : []
)
}>
<Remove />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
<Divider />
</React.Fragment>
);
})}
{!loading && !loadingMore && notifications.size > 0 && notifications.size < total && (
<ListItem onClick={() => setOffset(offset + 10)}>
<div style={{ margin: "auto" }}>
<Typography>Load More</Typography>
</div>
</ListItem>
)}{" "}
{loadingMore && (
<ListItem>
<div style={{ margin: "auto" }}>
<CircularProgress />
</div>
</ListItem>
)}{" "}
{!loadingMore && notifications.size > 0 && notifications.size >= total && (
<ListItem>
<div style={{ margin: "auto" }}>
<Typography color="textSecondary">End of list.</Typography>
</div>
</ListItem>
)}
</List>
</div>
</Drawer>
);
}