723 lines
32 KiB
TypeScript
723 lines
32 KiB
TypeScript
"use client";
|
|
import { useState, useEffect, useRef, Suspense } from "react";
|
|
import Secure from "../components/Secure/Secure";
|
|
import Alert from "@components/Alert/Alert";
|
|
import Header from "@components/Header/Header";
|
|
import { SystemNotificationsProvider } from "@components/SystemNotificationsProvider/SystemNotificationsProvider";
|
|
import UserMenu from "@components/UserMenu/UserMenu";
|
|
import style from "./style.module.css";
|
|
import useWindowSize from "../hooks/WindowSize";
|
|
import classNames from "classnames";
|
|
import Textbox from "../components/Textbox/Textbox";
|
|
import {
|
|
FindConversationItem,
|
|
IMessage,
|
|
ISendMessageResult,
|
|
MESSAGE_CONVERSATION_TYPES,
|
|
SendMessageParams,
|
|
} from "@core/Models/Messages.model";
|
|
import {
|
|
markConversationAsRead,
|
|
messagesFind,
|
|
messagesFindContacts,
|
|
messagesFindConversations,
|
|
sendMessage,
|
|
} from "./Messages.service";
|
|
import { useSessionStore } from "@core/Store/Sesion.Store";
|
|
import Avatar from "../components/Avatar/Avatar";
|
|
import SendIcon from "@mui/icons-material/Send";
|
|
import BackIcon from "@mui/icons-material/ArrowBack";
|
|
import { motion } from "motion/react";
|
|
import { IconButton } from "@mui/material";
|
|
import { EVENT_TYPES, useEventHandlerStore } from "@core/Store/EventHandler.Store";
|
|
import { useAlert } from "@core/Store/Alert.Store";
|
|
import { getChatClientSocket } from "./Messages.socket";
|
|
import { Socket } from "socket.io-client";
|
|
import dayjs from "dayjs";
|
|
|
|
import "dayjs/locale/es";
|
|
import { blurElementsOnLoad } from "@core/helpers/blur";
|
|
import HeaderConfProvider from "../components/HeaderConfProvider/HeaderConfProvider";
|
|
import SuspenseLoading from "../components/SuspendeLoading/SuspenseLoading";
|
|
|
|
dayjs.locale("es");
|
|
|
|
/**
|
|
* TODO:
|
|
* En la columna de conversaciones, vamos a poner en la parte superior una caja de texto
|
|
* que permita buscar organizaciones, y en el caso de las organizaciones que el usuario es cliente
|
|
* se tienen que mostrar los colaboradores de esa organizacion.
|
|
* Si el usuario es colaborador de la organizacion, se tienen que mostrar los clientes de esa organizacion
|
|
* y los otros colaboradores de la organizacion.
|
|
*/
|
|
|
|
enum VIEW_FOCUS {
|
|
CONVERSATIONS = "CONVERSATIONS",
|
|
MESSAGES = "MESSAGES",
|
|
}
|
|
|
|
enum MESSENGER_VIEWS {
|
|
CONVERSATIONS = "CONVERSATIONS",
|
|
MESSAGES = "MESSAGES",
|
|
BOTH = "BOTH",
|
|
}
|
|
|
|
type ViewClasses =
|
|
| "MessengerContainer"
|
|
| "MessengerBody"
|
|
| "ConversationsContainer"
|
|
| "ConversationsHeader"
|
|
| "ConversationsBody"
|
|
| "MessagesContainer"
|
|
| "MessagesHeader"
|
|
| "MessagesBody";
|
|
|
|
const classesMap = {
|
|
CONVERSATIONS: {
|
|
MessengerContainer: style.MessengerContainer_CONVERSATIONS,
|
|
MessengerBody: style.MessengerBody_CONVERSATIONS,
|
|
ConversationsContainer: style.ConversationsContainer_CONVERSATIONS,
|
|
ConversationsHeader: style.ConversationsHeader_CONVERSATIONS,
|
|
ConversationsBody: style.ConversationsBody_CONVERSATIONS,
|
|
MessagesContainer: style.MessagesContainer_CONVERSATIONS,
|
|
MessagesHeader: style.MessagesHeader_CONVERSATIONS,
|
|
MessagesBody: style.MessagesBody_CONVERSATIONS,
|
|
},
|
|
MESSAGES: {
|
|
MessengerContainer: style.MessengerContainer_MESSAGES,
|
|
MessengerBody: style.MessengerBody_MESSAGES,
|
|
ConversationsContainer: style.ConversationsContainer_MESSAGES,
|
|
ConversationsHeader: style.ConversationsHeader_MESSAGES,
|
|
ConversationsBody: style.ConversationsBody_MESSAGES,
|
|
MessagesContainer: style.MessagesContainer_MESSAGES,
|
|
MessagesHeader: style.MessagesHeader_MESSAGES,
|
|
MessagesBody: style.MessagesBody_MESSAGES,
|
|
},
|
|
BOTH: {
|
|
MessengerContainer: style.MessengerContainer_BOTH,
|
|
MessengerBody: style.MessengerBody_BOTH,
|
|
ConversationsContainer: style.ConversationsContainer_BOTH,
|
|
ConversationsHeader: style.ConversationsHeader_BOTH,
|
|
ConversationsBody: style.ConversationsBody_BOTH,
|
|
MessagesContainer: style.MessagesContainer_BOTH,
|
|
MessagesHeader: style.MessagesHeader_BOTH,
|
|
MessagesBody: style.MessagesBody_BOTH,
|
|
},
|
|
};
|
|
|
|
interface FindConversationParams {
|
|
selectConversationId?: string;
|
|
page?: number;
|
|
}
|
|
|
|
export default function Messages() {
|
|
const SessionInfo = useSessionStore();
|
|
const alert = useAlert();
|
|
const eventHandler = useEventHandlerStore();
|
|
const [width, height] = useWindowSize();
|
|
const [view, setView] = useState<MESSENGER_VIEWS>(MESSENGER_VIEWS.BOTH);
|
|
const [viewFocus, setViewFocus] = useState<VIEW_FOCUS>(VIEW_FOCUS.CONVERSATIONS);
|
|
const [conversations, setConversations] = useState<FindConversationItem[]>([]);
|
|
const [messages, setMessages] = useState<IMessage[]>([]);
|
|
const [selectedConversation, setSelectedConversation] = useState<
|
|
FindConversationItem | undefined
|
|
>(undefined);
|
|
const [filter, setFilter] = useState<string>("");
|
|
const [chatText, setChatText] = useState<string>("");
|
|
const chatBodyRef = useRef<HTMLDivElement>(null);
|
|
const [chatSocket, setChatSocket] = useState<Socket | undefined>(undefined);
|
|
const [newMessageReceived, setNewMessageReceived] = useState<ISendMessageResult | null>(null);
|
|
|
|
const [loadingChatNextPage, setLoadingChatNextPage] = useState<boolean>(false);
|
|
const [chatPage, setChatPage] = useState<number>(1);
|
|
|
|
const getClassName = (name: ViewClasses) => {
|
|
return classNames(style[name], classesMap[view][name]);
|
|
};
|
|
|
|
const getMessageClassName = (message: IMessage) => {
|
|
if (message.userId === SessionInfo.userId) {
|
|
return classNames(style.MessageContainerSender);
|
|
} else {
|
|
return classNames(style.MessageContainerReceiver);
|
|
}
|
|
};
|
|
|
|
const viewManager = () => {
|
|
if (width < 800) {
|
|
if (viewFocus === VIEW_FOCUS.CONVERSATIONS) {
|
|
setView(MESSENGER_VIEWS.CONVERSATIONS);
|
|
} else if (viewFocus === VIEW_FOCUS.MESSAGES) {
|
|
setView(MESSENGER_VIEWS.MESSAGES);
|
|
}
|
|
} else {
|
|
setView(MESSENGER_VIEWS.BOTH);
|
|
}
|
|
};
|
|
|
|
const findContacts = (query: string) => {
|
|
messagesFindContacts({
|
|
page: 1,
|
|
query: query,
|
|
sessionUser: SessionInfo.userId,
|
|
limit: 20,
|
|
})
|
|
.then((result) => {
|
|
setConversations(result.data);
|
|
})
|
|
.finally(() => {
|
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
|
});
|
|
};
|
|
|
|
const findConversations = ({ page = 1, selectConversationId = "" }: FindConversationParams) => {
|
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
|
messagesFindConversations({
|
|
page: page,
|
|
sessionUser: SessionInfo.userId,
|
|
limit: 20,
|
|
})
|
|
.then((result) => {
|
|
setConversations(result.data);
|
|
if (selectConversationId != "") {
|
|
setSelectedConversation(
|
|
result.data.find((item) => item.conversation.id === selectConversationId)
|
|
);
|
|
}
|
|
})
|
|
.finally(() => {
|
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
|
});
|
|
};
|
|
|
|
const loadConversationChat = (markAsRead: boolean = false) => {
|
|
if (!selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
if (!selectedConversation.conversation.id) {
|
|
setMessages([]);
|
|
return;
|
|
}
|
|
|
|
messagesFind({
|
|
conversationId: selectedConversation.conversation.id,
|
|
sessionUser: SessionInfo.userId,
|
|
page: 1,
|
|
limit: 20,
|
|
})
|
|
.then((result) => {
|
|
const messages = result.data;
|
|
const orderedMessages = messages.sort((a, b) => {
|
|
return new Date(a.messageDate).getTime() - new Date(b.messageDate).getTime();
|
|
});
|
|
setMessages(orderedMessages);
|
|
setChatPage(1);
|
|
if (markAsRead) {
|
|
setTimeout(() => {
|
|
markCurrentConversationAsReadHandler();
|
|
markConversationsAsReadHandler();
|
|
}, 2000);
|
|
}
|
|
})
|
|
.finally(() => {
|
|
chatScrollToEnd();
|
|
});
|
|
};
|
|
|
|
const loadConversationChatNextPage = () => {
|
|
if (!selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
if (!selectedConversation.conversation.id) {
|
|
setMessages([]);
|
|
return;
|
|
}
|
|
|
|
if (loadingChatNextPage) {
|
|
return;
|
|
}
|
|
|
|
setLoadingChatNextPage(true);
|
|
|
|
const chatBody = chatBodyRef.current;
|
|
const previousScrollHeight = chatBody?.scrollHeight || 0;
|
|
const previousScrollTop = chatBody?.scrollTop || 0;
|
|
|
|
messagesFind({
|
|
conversationId: selectedConversation.conversation.id,
|
|
sessionUser: SessionInfo.userId,
|
|
page: chatPage + 1,
|
|
limit: 20,
|
|
})
|
|
.then((result) => {
|
|
if (result.page > chatPage && result.data.length > 0) {
|
|
setChatPage(result.page);
|
|
const messagesResult = [...messages, ...result.data];
|
|
const orderedMessages = messagesResult.sort((a, b) => {
|
|
return (
|
|
new Date(a.messageDate).getTime() - new Date(b.messageDate).getTime()
|
|
);
|
|
});
|
|
setMessages(orderedMessages);
|
|
//Lleva dobre requestAnimationFrame porque sino no se entera del nuevo tamanio del div.
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
const chatBody = chatBodyRef.current;
|
|
if (chatBody) {
|
|
const newScrollHeight = chatBody.scrollHeight;
|
|
const delta = newScrollHeight - previousScrollHeight;
|
|
chatBody.scrollTop = previousScrollTop + delta;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
})
|
|
.finally(() => {
|
|
setLoadingChatNextPage(false);
|
|
});
|
|
};
|
|
|
|
const send = () => {
|
|
if (!selectedConversation) {
|
|
return;
|
|
}
|
|
|
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
|
|
|
const conversationType = selectedConversation.conversation.type;
|
|
|
|
const sendMessageData: SendMessageParams = {
|
|
conversationId: selectedConversation.conversation.id
|
|
? selectedConversation.conversation.id
|
|
: undefined,
|
|
type: conversationType,
|
|
userIdFrom: SessionInfo.userId,
|
|
userIdTo:
|
|
conversationType == MESSAGE_CONVERSATION_TYPES.USER_TO_USER
|
|
? selectedConversation.conversation.userIdTo
|
|
: undefined,
|
|
companyId:
|
|
conversationType == MESSAGE_CONVERSATION_TYPES.USER_TO_ORG
|
|
? selectedConversation.conversation.companyId
|
|
: undefined,
|
|
message: chatText,
|
|
sessionUser: SessionInfo.userId,
|
|
};
|
|
|
|
sendMessage(sendMessageData)
|
|
.then((result) => {
|
|
if (!sendMessageData.conversationId) {
|
|
findConversations({
|
|
page: 1,
|
|
selectConversationId: result.conversation.id,
|
|
});
|
|
return;
|
|
}
|
|
|
|
loadConversationChat();
|
|
})
|
|
.catch((error) => {
|
|
alert.showError(error.format());
|
|
})
|
|
.finally(() => {
|
|
chatScrollToEnd();
|
|
setChatText("");
|
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
|
});
|
|
};
|
|
|
|
const conversationSelectedHandler = (item: FindConversationItem) => {
|
|
if (view != MESSENGER_VIEWS.BOTH) {
|
|
setView(MESSENGER_VIEWS.MESSAGES);
|
|
setViewFocus(VIEW_FOCUS.MESSAGES);
|
|
}
|
|
setSelectedConversation(item);
|
|
setChatText("");
|
|
setFilter("");
|
|
};
|
|
|
|
const chatScrollToEnd = () => {
|
|
requestAnimationFrame(() => {
|
|
const chatBody = chatBodyRef.current;
|
|
if (chatBody) {
|
|
chatBody.scrollTop = chatBody.scrollHeight;
|
|
}
|
|
});
|
|
};
|
|
|
|
const chatScrollHandler = () => {
|
|
const chatBody = chatBodyRef.current;
|
|
if (chatBody && chatBody.scrollTop <= 50) {
|
|
loadConversationChatNextPage();
|
|
}
|
|
};
|
|
|
|
const updateOtherConversationsReadedCounter = (data: ISendMessageResult) => {
|
|
const conversation = conversations.find(
|
|
(c) => c.conversation.id === data.message.conversationId
|
|
);
|
|
if (conversation) {
|
|
const newMessagesCount = conversation.conversation.newMessages
|
|
? conversation.conversation.newMessages + 1
|
|
: 1;
|
|
conversation.conversation.newMessages = newMessagesCount;
|
|
const indexConversation = conversations.indexOf(conversation);
|
|
const conversationsUpdated = [...conversations];
|
|
conversationsUpdated[indexConversation] = conversation;
|
|
setConversations(conversationsUpdated);
|
|
}
|
|
};
|
|
|
|
const newMessageReceivedHandler = (data: ISendMessageResult) => {
|
|
if (!selectedConversation) {
|
|
updateOtherConversationsReadedCounter(data);
|
|
return;
|
|
}
|
|
|
|
if (selectedConversation.conversation.id != data.message.conversationId) {
|
|
updateOtherConversationsReadedCounter(data);
|
|
return;
|
|
}
|
|
|
|
const updatedMessages = [...messages, ...[data.message]];
|
|
setMessages(updatedMessages);
|
|
chatScrollToEnd();
|
|
};
|
|
|
|
const markConversationsAsReadHandler = () => {
|
|
if (!selectedConversation || !selectedConversation.conversation.id) {
|
|
return;
|
|
}
|
|
|
|
if (!SessionInfo.userId) {
|
|
return;
|
|
}
|
|
|
|
markConversationAsRead({
|
|
conversationId: selectedConversation.conversation.id,
|
|
sessionUser: SessionInfo.userId,
|
|
});
|
|
};
|
|
|
|
const markCurrentConversationAsReadHandler = () => {
|
|
if (!selectedConversation || !selectedConversation.conversation.id) {
|
|
return;
|
|
}
|
|
|
|
const conversation = conversations.find(
|
|
(c) => c.conversation.id === selectedConversation.conversation.id
|
|
);
|
|
if (conversation) {
|
|
conversation.conversation.newMessages = 0;
|
|
const indexConversation = conversations.indexOf(conversation);
|
|
const conversationsUpdated = [...conversations];
|
|
conversationsUpdated[indexConversation] = conversation;
|
|
setConversations(conversationsUpdated);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
|
setSelectedConversation(undefined);
|
|
blurElementsOnLoad();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!SessionInfo.userId) {
|
|
return;
|
|
}
|
|
|
|
if (filter != "") {
|
|
findContacts(filter);
|
|
return;
|
|
}
|
|
|
|
findConversations({ page: 1 });
|
|
}, [SessionInfo.userId, filter]);
|
|
|
|
useEffect(() => {
|
|
if (!SessionInfo.userId) {
|
|
if (chatSocket) {
|
|
chatSocket.off("new_message");
|
|
chatSocket.disconnect();
|
|
}
|
|
return;
|
|
}
|
|
|
|
loadConversationChat(true);
|
|
|
|
if (chatSocket) {
|
|
return;
|
|
}
|
|
|
|
const socket = getChatClientSocket(SessionInfo.userId);
|
|
setChatSocket(socket);
|
|
}, [SessionInfo.userId, selectedConversation]);
|
|
|
|
useEffect(() => {
|
|
if (!chatSocket) {
|
|
return;
|
|
}
|
|
|
|
chatSocket.on("new_message", (message: ISendMessageResult) => {
|
|
setNewMessageReceived(message);
|
|
});
|
|
|
|
return () => {
|
|
chatSocket.off("new_message");
|
|
};
|
|
}, [chatSocket]);
|
|
|
|
useEffect(() => {
|
|
if (!newMessageReceived) {
|
|
return;
|
|
}
|
|
newMessageReceivedHandler(newMessageReceived);
|
|
}, [newMessageReceived]);
|
|
|
|
useEffect(() => {
|
|
viewManager();
|
|
}, [width, height]);
|
|
return (
|
|
<Suspense fallback={<SuspenseLoading />}>
|
|
<div>
|
|
<Header />
|
|
<HeaderConfProvider />
|
|
<SystemNotificationsProvider />
|
|
<Secure>
|
|
<div className={getClassName("MessengerContainer")}>
|
|
<div className={getClassName("MessengerBody")}>
|
|
<div className={getClassName("ConversationsContainer")}>
|
|
<div className={getClassName("ConversationsHeader")}>
|
|
<Textbox
|
|
type="text"
|
|
value={filter}
|
|
width="100%"
|
|
onChange={(e) => setFilter(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className={getClassName("ConversationsBody")}>
|
|
{conversations.length === 0 && (
|
|
<div className={style.NoContacts}>
|
|
No hay conversaciones activas. Puede buscar contactos en
|
|
el cuadro de busqueda superior.
|
|
</div>
|
|
)}
|
|
{conversations.map((item, index) => {
|
|
return (
|
|
<motion.div
|
|
key={"conversation." + index}
|
|
className="w-64 h-40 rounded-2xl cursor-pointer"
|
|
initial={{ backgroundColor: "var(--white)" }}
|
|
whileHover={{
|
|
backgroundColor: "var(--gray-lighter)",
|
|
color: "var(--black)",
|
|
}} // Azul claro en hover
|
|
whileTap={{
|
|
backgroundColor: "var(--wine-dark)",
|
|
color: "white",
|
|
}} // Azul más fuerte en tap
|
|
transition={{ duration: 0.2 }}
|
|
>
|
|
<div
|
|
className={style.ConversationItem}
|
|
onClick={() => {
|
|
conversationSelectedHandler(item);
|
|
}}
|
|
>
|
|
<div>
|
|
<Avatar
|
|
src={item.image}
|
|
alt={item.name}
|
|
size="medium"
|
|
border="none"
|
|
/>
|
|
</div>
|
|
<div style={{ width: "100%" }}>
|
|
<div
|
|
className={style.ConversationItemTitle}
|
|
>
|
|
{item.name}
|
|
</div>
|
|
<div
|
|
className={
|
|
style.ConversationItemDescription
|
|
}
|
|
>
|
|
{item.description}
|
|
</div>
|
|
</div>
|
|
{item.conversation.newMessages > 0 && (
|
|
<div
|
|
className={
|
|
style.ConversationItemNewMessages
|
|
}
|
|
>
|
|
{item.conversation.newMessages > 0
|
|
? item.conversation.newMessages
|
|
: "-1"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className={getClassName("MessagesContainer")}>
|
|
{!selectedConversation && (
|
|
<div className={style.NoConversation}>
|
|
Seleccione un conversación
|
|
</div>
|
|
)}
|
|
{selectedConversation && (
|
|
<>
|
|
<div className={getClassName("MessagesHeader")}>
|
|
{selectedConversation && (
|
|
<div className={style.MessageHeaderLeft}>
|
|
{view === MESSENGER_VIEWS.MESSAGES && (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<IconButton
|
|
onClick={() => {
|
|
setView(
|
|
MESSENGER_VIEWS.CONVERSATIONS
|
|
);
|
|
setViewFocus(
|
|
VIEW_FOCUS.CONVERSATIONS
|
|
);
|
|
}}
|
|
>
|
|
<BackIcon />
|
|
</IconButton>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<Avatar
|
|
src={selectedConversation.image}
|
|
alt={selectedConversation.name}
|
|
size="small"
|
|
border="none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div
|
|
className={style.MessageHeaderLeftTitle}
|
|
>
|
|
{selectedConversation.name}
|
|
</div>
|
|
<div
|
|
className={
|
|
style.MessageHeaderLeftDescription
|
|
}
|
|
>
|
|
{selectedConversation.description}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div
|
|
ref={chatBodyRef}
|
|
className={getClassName("MessagesBody")}
|
|
onScroll={chatScrollHandler}
|
|
>
|
|
<div className={style.MessageChatContainer}>
|
|
{messages.map((item, index) => {
|
|
const showDate =
|
|
index === 0 ||
|
|
!dayjs(item.messageDate)
|
|
.startOf("day")
|
|
.isSame(
|
|
dayjs(
|
|
messages[
|
|
index - 1
|
|
].messageDate.toString()
|
|
).startOf("day")
|
|
);
|
|
|
|
return (
|
|
<div key={"message." + index}>
|
|
{showDate && (
|
|
<div
|
|
className={
|
|
style.MessageChatDateContainer
|
|
}
|
|
>
|
|
<div
|
|
className={
|
|
style.MessageChatDate
|
|
}
|
|
>
|
|
{dayjs(
|
|
item.messageDate
|
|
).format(
|
|
"dddd DD [de] MMMM, YYYY"
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div
|
|
className={getMessageClassName(
|
|
item
|
|
)}
|
|
>
|
|
<div
|
|
key={"message." + index}
|
|
className={
|
|
style.MessageChatItem
|
|
}
|
|
>
|
|
<div>{item.message}</div>
|
|
<div>
|
|
{dayjs(
|
|
item.messageDate
|
|
).format("HH:mm")}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className={style.MessageBottomBar}>
|
|
<Textbox
|
|
type="text"
|
|
value={chatText}
|
|
width="100%"
|
|
onChange={(e) => {
|
|
setChatText(e.target.value);
|
|
}}
|
|
onKeyUp={(e) => {
|
|
if (e.key === "Enter") {
|
|
send();
|
|
}
|
|
}}
|
|
/>
|
|
<IconButton color="success" onClick={send}>
|
|
<SendIcon />
|
|
</IconButton>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Secure>
|
|
|
|
<UserMenu />
|
|
<Alert />
|
|
</div>
|
|
</Suspense>
|
|
);
|
|
}
|