added notification drawer to the header
for the moment commented out the code to get the component icons and am just displaying the device icons until the component stuff is added to the new frontend
This commit is contained in:
parent
67ea4cbfdc
commit
38e0bcb33c
5 changed files with 487 additions and 11 deletions
|
|
@ -1,11 +1,14 @@
|
|||
import { Chat as ChatIcon } from "@mui/icons-material";
|
||||
import { Chat as ChatIcon, Notifications } from "@mui/icons-material";
|
||||
import { Box, IconButton, Theme, Tooltip } from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { ChatDrawer } from "chat/ChatDrawer";
|
||||
import { Team, User } from "models";
|
||||
import moment from "moment";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getSignatureAccentColour } from "services/whiteLabel";
|
||||
import NotificationDrawer from "./NotificationDrawer";
|
||||
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
button: {
|
||||
|
|
@ -47,9 +50,11 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function HeaderButtons(props: Props) {
|
||||
const {hasTeams, /*user,*/ team} = props
|
||||
const {hasTeams, user, team} = props
|
||||
const [hasNewChats, setHasNewChats] = useState(false);
|
||||
const [hasNewNotifications, setHasNewNotifications] = useState(false);
|
||||
const [chatDrawerOpen, setChatDrawerOpen] = useState(false);
|
||||
const [notificationDrawerOpen, setNotificationDrawerOpen] = useState<boolean>(false);
|
||||
const classes = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -70,6 +75,30 @@ export default function HeaderButtons(props: Props) {
|
|||
}
|
||||
}, [team]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
if (!user.status) return;
|
||||
if (
|
||||
user.status.lastNotificationViewed.length < 1 &&
|
||||
user.status.lastNotifiedTimestamp.length > 1
|
||||
) {
|
||||
setHasNewNotifications(true);
|
||||
return;
|
||||
}
|
||||
let viewMoment = moment(new Date(user.status.lastNotificationViewed));
|
||||
let notiMoment = moment(new Date(user.status.lastNotifiedTimestamp));
|
||||
if (viewMoment.diff(notiMoment) < 0) {
|
||||
setHasNewNotifications(true);
|
||||
} else {
|
||||
setHasNewNotifications(false);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
// If the drawer has been opened, get rid of the ping
|
||||
useEffect(() => {
|
||||
if (notificationDrawerOpen) setHasNewNotifications(false);
|
||||
}, [notificationDrawerOpen]);
|
||||
|
||||
const chatButton = () => {
|
||||
if (!hasTeams) return;
|
||||
if (team.key().length < 1) return
|
||||
|
|
@ -83,17 +112,44 @@ export default function HeaderButtons(props: Props) {
|
|||
setHasNewChats(false);
|
||||
}}>
|
||||
<ChatIcon />
|
||||
{hasNewChats && (
|
||||
<Box
|
||||
width="8px"
|
||||
height="8px"
|
||||
borderRadius="50%"
|
||||
className={classes.on}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{hasNewChats && (
|
||||
<Box
|
||||
width="8px"
|
||||
height="8px"
|
||||
borderRadius="50%"
|
||||
className={classes.on}
|
||||
/>
|
||||
)}
|
||||
const notificationButton = () => {
|
||||
if (!user.settings?.notificationMethods?.includes(
|
||||
pond.NotificationMethod.NOTIFICATION_METHOD_APP
|
||||
)) return
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Notifications">
|
||||
<IconButton
|
||||
className={classes.button}
|
||||
onClick={() => {
|
||||
setNotificationDrawerOpen(true);
|
||||
setHasNewChats(false);
|
||||
}}>
|
||||
<Notifications />
|
||||
{hasNewNotifications && (
|
||||
<Box
|
||||
width="8px"
|
||||
height="8px"
|
||||
borderRadius="50%"
|
||||
className={classes.on}
|
||||
/>
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -108,6 +164,11 @@ export default function HeaderButtons(props: Props) {
|
|||
team={team}
|
||||
/>
|
||||
)}
|
||||
{notificationButton()}
|
||||
<NotificationDrawer
|
||||
setNotificationDrawerOpen={setNotificationDrawerOpen}
|
||||
notificationDrawerOpen={notificationDrawerOpen}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
296
src/app/NotificationDrawer.tsx
Normal file
296
src/app/NotificationDrawer.tsx
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
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 + "/" + 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ export {
|
|||
useBinAPI,
|
||||
// useBinYardAPI,
|
||||
useNoteAPI,
|
||||
useNotificationAPI,
|
||||
// useComponentAPI,
|
||||
// useComponentsWebsocket,
|
||||
// useComponentWebsocket,
|
||||
|
|
|
|||
114
src/providers/pond/notificationAPI.tsx
Normal file
114
src/providers/pond/notificationAPI.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { useHTTP } from "hooks";
|
||||
import React, { createContext, PropsWithChildren, useContext } from "react";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { or } from "utils/types";
|
||||
import { pondURL } from "./pond";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
export interface INotificationAPIContext {
|
||||
listNotifications: (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
as?: string,
|
||||
asRoot?: boolean
|
||||
) => Promise<AxiosResponse<pond.ListNotificationsResponse>>;
|
||||
hideNotification: (keys: string[]) => Promise<AxiosResponse<pond.HideNotificationResponse>>;
|
||||
listObjectNotifications: (
|
||||
key: string,
|
||||
type: pond.ObjectType,
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
as?: string
|
||||
) => Promise<AxiosResponse<pond.ListObjectNotificationsResponse>>;
|
||||
}
|
||||
|
||||
export const NotificationAPIContext = createContext<INotificationAPIContext>(
|
||||
{} as INotificationAPIContext
|
||||
);
|
||||
|
||||
interface Props {}
|
||||
|
||||
export default function NotificationProvider(props: PropsWithChildren<Props>) {
|
||||
const { children } = props;
|
||||
const { get, put } = useHTTP();
|
||||
|
||||
const listNotifications = (
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
as?: string,
|
||||
asRoot?: boolean
|
||||
) => {
|
||||
return get<pond.ListNotificationsResponse>(
|
||||
pondURL(
|
||||
"/notifications" +
|
||||
"?limit=" +
|
||||
limit +
|
||||
"&offset=" +
|
||||
offset +
|
||||
("&order=" + or(order, "asc")) +
|
||||
("&by=" + or(orderBy, "key")) +
|
||||
(as ? "&as=" + as : "") +
|
||||
(asRoot ? "&asRoot=" + asRoot.toString() : "") +
|
||||
(search ? "&search=" + search : "")
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const listObjectNotifications = (
|
||||
key: string,
|
||||
type: pond.ObjectType,
|
||||
limit: number,
|
||||
offset: number,
|
||||
order?: "asc" | "desc",
|
||||
orderBy?: string,
|
||||
search?: string,
|
||||
as?: string
|
||||
) => {
|
||||
return get<pond.ListObjectNotificationsResponse>(
|
||||
pondURL(
|
||||
"/object/notifications" +
|
||||
"?objectKey=" +
|
||||
key +
|
||||
"&objectType=" +
|
||||
type +
|
||||
"&limit=" +
|
||||
limit +
|
||||
"&offset=" +
|
||||
offset +
|
||||
("&order=" + or(order, "asc")) +
|
||||
("&by=" + or(orderBy, "key")) +
|
||||
(search ? "&search=" + search : "") +
|
||||
(as ? "&as=" + as : "")
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const hideNotification = (keys: string[]) => {
|
||||
return put<pond.HideNotificationResponse>(
|
||||
pondURL("/notifications/hide"),
|
||||
pond.NotificationKeys.create({ keys: keys })
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationAPIContext.Provider
|
||||
value={{
|
||||
listNotifications,
|
||||
hideNotification,
|
||||
listObjectNotifications
|
||||
}}>
|
||||
{children}
|
||||
</NotificationAPIContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useNotificationAPI = () => useContext(NotificationAPIContext);
|
||||
|
|
@ -10,6 +10,7 @@ import NoteProvider, { useNoteAPI } from "./noteAPI";
|
|||
import DeviceProvider, { useDeviceAPI } from "./deviceAPI";
|
||||
import BackpackProvider, { useBackpackAPI } from "./backpackAPI";
|
||||
import GroupProvider, { useGroupAPI } from "./groupAPI";
|
||||
import NotificationProvider, { useNotificationAPI } from "./notificationAPI";
|
||||
// import NoteProvider from "providers/noteAPI";
|
||||
|
||||
export const pondURL = (partial: string, demo: boolean = false): string => {
|
||||
|
|
@ -36,7 +37,9 @@ export default function PondProvider(props: PropsWithChildren<any>) {
|
|||
<DeviceProvider>
|
||||
<GroupProvider>
|
||||
<BackpackProvider>
|
||||
{children}
|
||||
<NotificationProvider>
|
||||
{children}
|
||||
</NotificationProvider>
|
||||
</BackpackProvider>
|
||||
</GroupProvider>
|
||||
</DeviceProvider>
|
||||
|
|
@ -60,4 +63,5 @@ export {
|
|||
useGateAPI,
|
||||
useGroupAPI,
|
||||
useNoteAPI,
|
||||
useNotificationAPI
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue