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
This commit is contained in:

committed by
GitHub

parent
c643bdcfce
commit
c596d38d7a
@@ -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.
|
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 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
|
### Development
|
||||||
|
|
||||||
|
BIN
screenshots/etke.cc/server-notifications/badge.webp
Normal file
BIN
screenshots/etke.cc/server-notifications/badge.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
BIN
screenshots/etke.cc/server-notifications/page.webp
Normal file
BIN
screenshots/etke.cc/server-notifications/page.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
@@ -25,6 +25,7 @@ import authProvider from "./synapse/authProvider";
|
|||||||
import dataProvider from "./synapse/dataProvider";
|
import dataProvider from "./synapse/dataProvider";
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
import ServerStatusPage from "./components/etke.cc/ServerStatusPage";
|
||||||
|
import ServerNotificationsPage from "./components/etke.cc/ServerNotificationsPage";
|
||||||
|
|
||||||
// TODO: Can we use lazy loading together with browser locale?
|
// TODO: Can we use lazy loading together with browser locale?
|
||||||
const messages = {
|
const messages = {
|
||||||
@@ -65,6 +66,7 @@ export const App = () => (
|
|||||||
<CustomRoutes>
|
<CustomRoutes>
|
||||||
<Route path="/import_users" element={<UserImport />} />
|
<Route path="/import_users" element={<UserImport />} />
|
||||||
<Route path="/server_status" element={<ServerStatusPage />} />
|
<Route path="/server_status" element={<ServerStatusPage />} />
|
||||||
|
<Route path="/server_notifications" element={<ServerNotificationsPage />} />
|
||||||
</CustomRoutes>
|
</CustomRoutes>
|
||||||
<Resource {...users} />
|
<Resource {...users} />
|
||||||
<Resource {...rooms} />
|
<Resource {...rooms} />
|
||||||
|
@@ -5,6 +5,7 @@ import { Icons, DefaultIcon } from "../utils/icons";
|
|||||||
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
import { MenuItem, GetConfig, ClearConfig } from "../utils/config";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
import ServerStatusBadge from "./etke.cc/ServerStatusBadge";
|
||||||
|
import { ServerNotificationsBadge } from "./etke.cc/ServerNotificationsBadge";
|
||||||
|
|
||||||
const AdminUserMenu = () => {
|
const AdminUserMenu = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -50,6 +51,7 @@ const AdminAppBar = () => {
|
|||||||
return (<AppBar userMenu={<AdminUserMenu />}>
|
return (<AppBar userMenu={<AdminUserMenu />}>
|
||||||
<TitlePortal />
|
<TitlePortal />
|
||||||
<ServerStatusBadge />
|
<ServerStatusBadge />
|
||||||
|
<ServerNotificationsBadge />
|
||||||
<InspectorButton />
|
<InspectorButton />
|
||||||
</AppBar>);
|
</AppBar>);
|
||||||
};
|
};
|
||||||
|
@@ -28,3 +28,15 @@ Server Status page. This page contains the following information:
|
|||||||
* Overall server status (up/updating/has issues)
|
* Overall server status (up/updating/has issues)
|
||||||
* Details about the currently running command (if any)
|
* 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
|
* Details about the server's components statuses (up/down with error details and suggested actions) by categories
|
||||||
|
|
||||||
|
### Server Notifications icon
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the application bar the new notifications icon is displayed that shows the number of unread (not removed) notifications
|
||||||
|
|
||||||
|
### Server Notifications page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.
|
||||||
|
184
src/components/etke.cc/ServerNotificationsBadge.tsx
Normal file
184
src/components/etke.cc/ServerNotificationsBadge.tsx
Normal file
@@ -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<ServerNotificationsResponse>("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 | HTMLElement>(null);
|
||||||
|
const open = Boolean(anchorEl);
|
||||||
|
|
||||||
|
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
|
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 (
|
||||||
|
<Box>
|
||||||
|
<IconButton onClick={handleOpen} sx={{ color: theme.palette.common.white }}>
|
||||||
|
<Tooltip title={notifications && notifications.length > 0 ? `${notifications.length} new notifications` : `No notifications yet`}>
|
||||||
|
{notifications && notifications.length > 0 && (
|
||||||
|
<Badge badgeContent={notifications.length} color="error">
|
||||||
|
<NotificationsIcon />
|
||||||
|
</Badge>
|
||||||
|
) || <NotificationsIcon />}
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
placement="bottom-end"
|
||||||
|
style={{ zIndex: 1300 }}
|
||||||
|
>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<Paper
|
||||||
|
elevation={3}
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
maxHeight: "350px",
|
||||||
|
overflowY: "auto",
|
||||||
|
minWidth: "300px",
|
||||||
|
maxWidth: {
|
||||||
|
xs: "100vw", // Full width on mobile
|
||||||
|
sm: "400px" // Fixed width on desktop
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(!notifications || notifications.length === 0) ? (
|
||||||
|
<Typography sx={{ p: 1 }} variant="body2">No new notifications</Typography>
|
||||||
|
) : (
|
||||||
|
<List sx={{ p: 0 }} dense={true}>
|
||||||
|
<ListSubheader
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
fontWeight: "bold",
|
||||||
|
backgroundColor: "inherit",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6">Notifications</Typography>
|
||||||
|
<Box sx={{ cursor: "pointer", color: theme.palette.primary.main }} onClick={() => handleSeeAllNotifications()}>See all notifications</Box>
|
||||||
|
</ListSubheader>
|
||||||
|
<Divider />
|
||||||
|
{notifications.map((notification, index) => {
|
||||||
|
return (<Fragment key={notification.event_id ? notification.event_id : index }>
|
||||||
|
<ListItem
|
||||||
|
onClick={() => handleSeeAllNotifications()}
|
||||||
|
sx={{
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "action.hover",
|
||||||
|
cursor: "pointer"
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}
|
||||||
|
dangerouslySetInnerHTML={{ __html: notification.output.split("\n")[0] }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
</Fragment>
|
||||||
|
)})}
|
||||||
|
<ListItem>
|
||||||
|
<Button
|
||||||
|
key="clear-all-notifications"
|
||||||
|
onClick={() => handleClearAllNotifications()}
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
sx={{
|
||||||
|
pl: 0,
|
||||||
|
pt: 1,
|
||||||
|
verticalAlign: "middle"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
|
||||||
|
Clear all
|
||||||
|
</Button>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Popper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
58
src/components/etke.cc/ServerNotificationsPage.tsx
Normal file
58
src/components/etke.cc/ServerNotificationsPage.tsx
Normal file
@@ -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<ServerNotificationsResponse>("serverNotifications", {
|
||||||
|
notifications: [],
|
||||||
|
success: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const notifications = serverNotifications.notifications;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={3} mt={3}>
|
||||||
|
<Stack spacing={1} direction="row" alignItems="center">
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%", gap: 1 }}>
|
||||||
|
<Typography variant="h4">Server Notifications</Typography>
|
||||||
|
<Button variant="text" color="error" onClick={async () => {
|
||||||
|
await dataProvider.deleteServerNotifications(etkeccAdmin);
|
||||||
|
setServerNotifications({
|
||||||
|
notifications: [],
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<DeleteIcon fontSize="small" sx={{ mr: 1 }} /> Clear
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{notifications.length === 0 ? (
|
||||||
|
<Paper sx={{ p: 2 }}>
|
||||||
|
<Typography>No new notifications.</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
notifications.map((notification, index) => (
|
||||||
|
<Paper key={notification.event_id ? notification.event_id : index} sx={{ p: 2 }}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold" color="text.secondary">
|
||||||
|
<DisplayTime date={notification.sent_at} />
|
||||||
|
</Typography>
|
||||||
|
<Typography dangerouslySetInnerHTML={{ __html: notification.output }} />
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServerNotificationsPage;
|
@@ -80,11 +80,7 @@ const authProvider: AuthProvider = {
|
|||||||
localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
localStorage.setItem("access_token", accessToken ? accessToken : json.access_token);
|
||||||
localStorage.setItem("device_id", json.device_id);
|
localStorage.setItem("device_id", json.device_id);
|
||||||
localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
localStorage.setItem("login_type", accessToken ? "accessToken" : "credentials");
|
||||||
|
await FetchConfig();
|
||||||
// when doing access token auth, config is not fetched, so we need to do it here
|
|
||||||
if (accessToken) {
|
|
||||||
await FetchConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve({redirectTo: "/"});
|
return Promise.resolve({redirectTo: "/"});
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
@@ -291,6 +291,17 @@ export interface ServerProcessResponse {
|
|||||||
command?: string;
|
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 {
|
export interface SynapseDataProvider extends DataProvider {
|
||||||
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
deleteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
purgeRemoteMedia: (params: DeleteMediaParams) => Promise<DeleteMediaResult>;
|
||||||
@@ -302,6 +313,8 @@ export interface SynapseDataProvider extends DataProvider {
|
|||||||
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
makeRoomAdmin: (room_id: string, user_id: string) => Promise<{ success: boolean; error?: string; errcode?: string }>;
|
||||||
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
getServerRunningProcess: (etkeAdminUrl: string) => Promise<ServerProcessResponse>;
|
||||||
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
getServerStatus: (etkeAdminUrl: string) => Promise<ServerStatusResponse>;
|
||||||
|
getServerNotifications: (etkeAdminUrl: string) => Promise<ServerNotificationsResponse>;
|
||||||
|
deleteServerNotifications: (etkeAdminUrl: string) => Promise<{ success: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceMap = {
|
const resourceMap = {
|
||||||
@@ -995,6 +1008,60 @@ const baseDataProvider: SynapseDataProvider = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, ok: false, host: "", results: [] };
|
return { success: false, ok: false, host: "", results: [] };
|
||||||
|
},
|
||||||
|
getServerNotifications: async (serverNotificationsUrl: string): Promise<ServerNotificationsResponse> => {
|
||||||
|
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 };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -53,7 +53,6 @@ export const FetchConfig = async () => {
|
|||||||
console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e);
|
console.log(`${protocol}://${homeserver}/.well-known/matrix/client not found, skipping`, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// load config from context
|
// load config from context
|
||||||
|
@@ -42,19 +42,20 @@ export function generateRandomMXID(): string {
|
|||||||
* @returns full MXID as string
|
* @returns full MXID as string
|
||||||
*/
|
*/
|
||||||
export function returnMXID(input: string | Identifier): 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
|
// when homeserver is not (just) a domain name, but a domain:port or even an IPv6 address
|
||||||
if (input.endsWith(homeserver) && input.startsWith("@")) {
|
if (homeserver != "" && inputStr.endsWith(homeserver) && inputStr.startsWith("@")) {
|
||||||
return input as string; // Already a valid MXID
|
return inputStr; // Already a valid MXID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
// Check if the input already looks like a valid MXID (i.e., starts with "@" and contains ":")
|
||||||
if (isMXID(input)) {
|
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
|
// 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}`;
|
return `@${localpart}:${homeserver}`;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user