From c596d38d7afa0f24446ccc2cb30e6168964aacb6 Mon Sep 17 00:00:00 2001 From: Borislav Pantaleev Date: Thu, 19 Dec 2024 11:24:42 +0200 Subject: [PATCH] Add notifications badge and page (#240) * WIP on server notifications * WIP: Add server notifications page and removal of notifications * improve design * fix missing notifications case; add tooltop * Fix api response * fix tests * add docs; update readme --- README.md | 1 + .../etke.cc/server-notifications/badge.webp | Bin 0 -> 8926 bytes .../etke.cc/server-notifications/page.webp | Bin 0 -> 25238 bytes src/App.tsx | 2 + src/components/AdminLayout.tsx | 2 + src/components/etke.cc/README.md | 12 ++ .../etke.cc/ServerNotificationsBadge.tsx | 184 ++++++++++++++++++ .../etke.cc/ServerNotificationsPage.tsx | 58 ++++++ src/synapse/authProvider.ts | 6 +- src/synapse/dataProvider.ts | 67 +++++++ src/utils/config.ts | 1 - src/utils/mxid.ts | 11 +- 12 files changed, 333 insertions(+), 11 deletions(-) create mode 100644 screenshots/etke.cc/server-notifications/badge.webp create mode 100644 screenshots/etke.cc/server-notifications/page.webp create mode 100644 src/components/etke.cc/ServerNotificationsBadge.tsx create mode 100644 src/components/etke.cc/ServerNotificationsPage.tsx diff --git a/README.md b/README.md index 06148a2..484b7ed 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ We at [etke.cc](https://etke.cc) attempting to develop everything open-source, b The following list contains such features - they are only available for [etke.cc](https://etke.cc) customers. * 📊 [Server Status indicator and page](https://github.com/etkecc/synapse-admin/pull/182) +* 📬 [Server Notifications indicator and page](https://github.com/etkecc/synapse-admin/pull/240) ### Development diff --git a/screenshots/etke.cc/server-notifications/badge.webp b/screenshots/etke.cc/server-notifications/badge.webp new file mode 100644 index 0000000000000000000000000000000000000000..6c351a59f1fe25b474a6b60ff457eae99f996b7d GIT binary patch literal 8926 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 ( } /> } /> + } /> diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx index 30eaaa0..b87ddb6 100644 --- a/src/components/AdminLayout.tsx +++ b/src/components/AdminLayout.tsx @@ -5,6 +5,7 @@ import { Icons, DefaultIcon } from "../utils/icons"; import { MenuItem, GetConfig, ClearConfig } from "../utils/config"; import Footer from "./Footer"; import ServerStatusBadge from "./etke.cc/ServerStatusBadge"; +import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge"; const AdminUserMenu = () => { const [open, setOpen] = useState(false); @@ -50,6 +51,7 @@ const AdminAppBar = () => { return (}> + ); }; diff --git a/src/components/etke.cc/README.md b/src/components/etke.cc/README.md index b7dfdf7..21afe85 100644 --- a/src/components/etke.cc/README.md +++ b/src/components/etke.cc/README.md @@ -28,3 +28,15 @@ Server Status page. This page contains the following information: * Overall server status (up/updating/has issues) * Details about the currently running command (if any) * Details about the server's components statuses (up/down with error details and suggested actions) by categories + +### Server Notifications icon + +![Server Notifications icon](../../../screenshots/etke.cc/server-notifications/badge.webp) + +In the application bar the new notifications icon is displayed that shows the number of unread (not removed) notifications + +### Server Notifications page + +![Server Notifications Page](../../../screenshots/etke.cc/server-notifications/page.webp) + +When you click on a notification from the [Server Notifications icon](#server-notifications-icon)'s list in the application bar, you will be redirected to the Server Notifications page. This page contains the full text of all the notifications you have about your server. diff --git a/src/components/etke.cc/ServerNotificationsBadge.tsx b/src/components/etke.cc/ServerNotificationsBadge.tsx new file mode 100644 index 0000000..560cc3d --- /dev/null +++ b/src/components/etke.cc/ServerNotificationsBadge.tsx @@ -0,0 +1,184 @@ +import { Badge, useTheme, Button, Paper, Popper, ClickAwayListener, Box, List, ListItem, ListItemText, Typography, ListSubheader, IconButton, Divider, Tooltip } from "@mui/material"; +import NotificationsIcon from '@mui/icons-material/Notifications'; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useDataProvider, useStore } from "react-admin"; +import { useNavigate } from "react-router"; +import { Fragment, useEffect, useState } from "react"; +import { useAppContext } from "../../Context"; +import { ServerNotificationsResponse } from "../../synapse/dataProvider"; + +const SERVER_NOTIFICATIONS_INTERVAL_TIME = 300000; + +const useServerNotifications = () => { + const [serverNotifications, setServerNotifications] = useStore("serverNotifications", { notifications: [], success: false }); + const { etkeccAdmin } = useAppContext(); + const dataProvider = useDataProvider(); + const { notifications, success } = serverNotifications; + + const fetchNotifications = async () => { + const notificationsResponse: ServerNotificationsResponse = await dataProvider.getServerNotifications(etkeccAdmin); + setServerNotifications({ + ...notificationsResponse, + notifications: notificationsResponse.notifications, + success: notificationsResponse.success + }); + }; + + const deleteServerNotifications = async () => { + const deleteResponse = await dataProvider.deleteServerNotifications(etkeccAdmin); + if (deleteResponse.success) { + await fetchNotifications(); + } + }; + + useEffect(() => { + let serverNotificationsInterval: NodeJS.Timeout; + if (etkeccAdmin) { + fetchNotifications(); + setTimeout(() => { + // start the interval after the SERVER_NOTIFICATIONS_INTERVAL_TIME to avoid too many requests + serverNotificationsInterval = setInterval(fetchNotifications, SERVER_NOTIFICATIONS_INTERVAL_TIME); + }, SERVER_NOTIFICATIONS_INTERVAL_TIME); + } + + return () => { + if (serverNotificationsInterval) { + clearInterval(serverNotificationsInterval); + } + } + }, [etkeccAdmin]); + + return { success, notifications, deleteServerNotifications }; +}; + +export const ServerNotificationsBadge = () => { + const navigate = useNavigate(); + const { success, notifications, deleteServerNotifications } = useServerNotifications(); + const theme = useTheme(); + + // Modify menu state to work with Popper + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleOpen = (event: React.MouseEvent) => { + setAnchorEl(anchorEl ? null : event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const handleSeeAllNotifications = () => { + handleClose(); + navigate("/server_notifications"); + }; + + const handleClearAllNotifications = async () => { + deleteServerNotifications() + handleClose(); + }; + + if (!success) { + return null; + } + + return ( + + + 0 ? `${notifications.length} new notifications` : `No notifications yet`}> + {notifications && notifications.length > 0 && ( + + + + ) || } + + + + + + {(!notifications || notifications.length === 0) ? ( + No new notifications + ) : ( + + + Notifications + handleSeeAllNotifications()}>See all notifications + + + {notifications.map((notification, index) => { + return ( + handleSeeAllNotifications()} + sx={{ + "&:hover": { + backgroundColor: "action.hover", + cursor: "pointer" + } + }} + > + + } + /> + + + + )})} + + + + + )} + + + + + ); +}; diff --git a/src/components/etke.cc/ServerNotificationsPage.tsx b/src/components/etke.cc/ServerNotificationsPage.tsx new file mode 100644 index 0000000..2476af8 --- /dev/null +++ b/src/components/etke.cc/ServerNotificationsPage.tsx @@ -0,0 +1,58 @@ +import { Box, Typography, Paper, Button } from "@mui/material" +import { Stack } from "@mui/material" +import { useStore } from "react-admin" +import dataProvider, { ServerNotificationsResponse } from "../../synapse/dataProvider" +import { useAppContext } from "../../Context"; +import DeleteIcon from "@mui/icons-material/Delete"; +const DisplayTime = ({ date }: { date: string }) => { + const dateFromDateString = new Date(date); + return <>{dateFromDateString.toLocaleString()}; +}; + +const ServerNotificationsPage = () => { + const { etkeccAdmin } = useAppContext(); + const [serverNotifications, setServerNotifications] = useStore("serverNotifications", { + notifications: [], + success: false, + }); + + const notifications = serverNotifications.notifications; + + return ( + + + + Server Notifications + + + + + {notifications.length === 0 ? ( + + No new notifications. + + ) : ( + notifications.map((notification, index) => ( + + + + + + + + + )) + )} + + ); +}; + +export default ServerNotificationsPage; diff --git a/src/synapse/authProvider.ts b/src/synapse/authProvider.ts index d88c83c..9ee74aa 100644 --- a/src/synapse/authProvider.ts +++ b/src/synapse/authProvider.ts @@ -80,11 +80,7 @@ const authProvider: AuthProvider = { localStorage.setItem("access_token", accessToken ? accessToken : json.access_token); localStorage.setItem("device_id", json.device_id); localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials"); - - // when doing access token auth, config is not fetched, so we need to do it here - if (accessToken) { - await FetchConfig(); - } + await FetchConfig(); return Promise.resolve({redirectTo: "/"}); } catch(err) { diff --git a/src/synapse/dataProvider.ts b/src/synapse/dataProvider.ts index c163781..cd51e65 100644 --- a/src/synapse/dataProvider.ts +++ b/src/synapse/dataProvider.ts @@ -291,6 +291,17 @@ export interface ServerProcessResponse { command?: string; } +export interface ServerNotification { + event_id: string; + output: string; + sent_at: string; +} + +export interface ServerNotificationsResponse { + success: boolean; + notifications: ServerNotification[]; +} + export interface SynapseDataProvider extends DataProvider { deleteMedia: (params: DeleteMediaParams) => Promise; purgeRemoteMedia: (params: DeleteMediaParams) => Promise; @@ -302,6 +313,8 @@ export interface SynapseDataProvider extends DataProvider { makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>; getServerRunningProcess: (etkeAdminUrl: string) => Promise; getServerStatus: (etkeAdminUrl: string) => Promise; + getServerNotifications: (etkeAdminUrl: string) => Promise; + deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>; } const resourceMap = { @@ -995,6 +1008,60 @@ const baseDataProvider: SynapseDataProvider = { } return { success: false, ok: false, host: "", results: [] }; + }, + getServerNotifications: async (serverNotificationsUrl: string): Promise => { + try { + const response = await fetch(`${serverNotificationsUrl}/notifications`, { + headers: { + "Authorization": `Bearer ${localStorage.getItem("access_token")}` + } + }); + if (!response.ok) { + console.error(`Error getting server notifications: ${response.status} ${response.statusText}`); + return { success: false, notifications: [] }; + } + + const status = response.status; + if (status === 204) { + return { success: true, notifications: [] }; + } + + if (status === 200) { + const json = await response.json(); + const result = { success: true, notifications: json } as ServerNotificationsResponse; + return result; + } + + return { success: true, notifications: [] }; + } catch (error) { + console.error("Error getting server notifications", error); + } + + return { success: false, notifications: [] }; + }, + deleteServerNotifications: async (serverNotificationsUrl: string) => { + try { + const response = await fetch(`${serverNotificationsUrl}/notifications`, { + headers: { + "Authorization": `Bearer ${localStorage.getItem("access_token")}` + }, + method: "DELETE" + }); + if (!response.ok) { + console.error(`Error deleting server notifications: ${response.status} ${response.statusText}`); + return { success: false }; + } + + const status = response.status; + if (status === 204) { + const result = { success: true } + return result; + } + } catch (error) { + console.error("Error deleting server notifications", error); + } + + return { success: false }; } }; diff --git a/src/utils/config.ts b/src/utils/config.ts index e7fcf79..fe9c4e8 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -53,7 +53,6 @@ export const FetchConfig = async () => { console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e); } } - } // load config from context diff --git a/src/utils/mxid.ts b/src/utils/mxid.ts index 1f5ec7a..c66ad72 100644 --- a/src/utils/mxid.ts +++ b/src/utils/mxid.ts @@ -42,19 +42,20 @@ export function generateRandomMXID(): string { * @returns full MXID as string */ export function returnMXID(input: string | Identifier): string { - const homeserver = localStorage.getItem("home_server"); + const inputStr = input as string; + const homeserver = localStorage.getItem("home_server") || ""; // when homeserver is not (just) a domain name, but a domain:port or even an IPv6 address - if (input.endsWith(homeserver) && input.startsWith("@")) { - return input as string; // Already a valid MXID + if (homeserver != "" && inputStr.endsWith(homeserver) && inputStr.startsWith("@")) { + return inputStr; // Already a valid MXID } // Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":") if (isMXID(input)) { - return input as string; // Already a valid MXID + return inputStr; // Already a valid MXID } // If input is not a valid MXID, assume it's a localpart and construct the MXID - const localpart = typeof input === 'string' && input.startsWith('@') ? input.slice(1) : input; + const localpart = typeof input === 'string' && inputStr.startsWith('@') ? inputStr.slice(1) : inputStr; return `@${localpart}:${homeserver}`; }