mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-06-06 16:36:58 +00:00
783 lines
23 KiB
TypeScript
783 lines
23 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { ChatList } from './ChatList';
|
|
import Tiptap from './TipTap';
|
|
import { CustomButton } from '../../styles/App-styles';
|
|
import CircularProgress from '@mui/material/CircularProgress';
|
|
import { Box, ButtonBase, Input, Typography, useTheme } from '@mui/material';
|
|
import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar';
|
|
import { getNameInfo } from '../Group/Group';
|
|
import { Spacer } from '../../common/Spacer';
|
|
import { CustomizedSnackbars } from '../Snackbar/Snackbar';
|
|
import {
|
|
getBaseApiReact,
|
|
getBaseApiReactSocket,
|
|
pauseAllQueues,
|
|
resumeAllQueues,
|
|
} from '../../App';
|
|
import { getPublicKey } from '../../background';
|
|
import { useMessageQueue } from '../../MessageQueueContext';
|
|
import {
|
|
executeEvent,
|
|
subscribeToEvent,
|
|
unsubscribeFromEvent,
|
|
} from '../../utils/events';
|
|
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
|
import ShortUniqueId from 'short-unique-id';
|
|
import { ExitIcon } from '../../assets/Icons/ExitIcon';
|
|
import { ReplyPreview } from './MessageItem';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
const uid = new ShortUniqueId({ length: 5 });
|
|
|
|
export const ChatDirect = ({
|
|
myAddress,
|
|
isNewChat,
|
|
selectedDirect,
|
|
setSelectedDirect,
|
|
setNewChat,
|
|
getTimestampEnterChat,
|
|
myName,
|
|
balance,
|
|
close,
|
|
setMobileViewModeKeepOpen,
|
|
}) => {
|
|
const theme = useTheme();
|
|
const { t } = useTranslation(['auth', 'core', 'group']);
|
|
const { queueChats, addToQueue, processWithNewMessages } = useMessageQueue();
|
|
const [isFocusedParent, setIsFocusedParent] = useState(false);
|
|
const [onEditMessage, setOnEditMessage] = useState(null);
|
|
const [messages, setMessages] = useState([]);
|
|
const [isSending, setIsSending] = useState(false);
|
|
const [directToValue, setDirectToValue] = useState('');
|
|
const hasInitialized = useRef(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [openSnack, setOpenSnack] = useState(false);
|
|
const [infoSnack, setInfoSnack] = useState(null);
|
|
const [publicKeyOfRecipient, setPublicKeyOfRecipient] = useState('');
|
|
const hasInitializedWebsocket = useRef(false);
|
|
const [chatReferences, setChatReferences] = useState({});
|
|
const editorRef = useRef(null);
|
|
const socketRef = useRef(null);
|
|
const timeoutIdRef = useRef(null);
|
|
const [messageSize, setMessageSize] = useState(0);
|
|
const groupSocketTimeoutRef = useRef(null);
|
|
const [replyMessage, setReplyMessage] = useState(null);
|
|
const setEditorRef = (editorInstance) => {
|
|
editorRef.current = editorInstance;
|
|
};
|
|
const publicKeyOfRecipientRef = useRef(null);
|
|
|
|
const getPublicKeyFunc = async (address) => {
|
|
try {
|
|
const publicKey = await getPublicKey(address);
|
|
if (publicKeyOfRecipientRef.current !== selectedDirect?.address) return;
|
|
setPublicKeyOfRecipient(publicKey);
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
};
|
|
|
|
const tempMessages = useMemo(() => {
|
|
if (!selectedDirect?.address) return [];
|
|
if (queueChats[selectedDirect?.address]) {
|
|
return queueChats[selectedDirect?.address]?.filter(
|
|
(item) => !item?.chatReference
|
|
);
|
|
}
|
|
return [];
|
|
}, [selectedDirect?.address, queueChats]);
|
|
|
|
const tempChatReferences = useMemo(() => {
|
|
if (!selectedDirect?.address) return [];
|
|
if (queueChats[selectedDirect?.address]) {
|
|
return queueChats[selectedDirect?.address]?.filter(
|
|
(item) => !!item?.chatReference
|
|
);
|
|
}
|
|
return [];
|
|
}, [selectedDirect?.address, queueChats]);
|
|
|
|
useEffect(() => {
|
|
if (selectedDirect?.address) {
|
|
publicKeyOfRecipientRef.current = selectedDirect?.address;
|
|
getPublicKeyFunc(publicKeyOfRecipientRef.current);
|
|
}
|
|
}, [selectedDirect?.address]);
|
|
|
|
const middletierFunc = async (
|
|
data: any,
|
|
selectedDirectAddress: string,
|
|
myAddress: string
|
|
) => {
|
|
try {
|
|
if (hasInitialized.current) {
|
|
decryptMessages(data, true);
|
|
return;
|
|
}
|
|
hasInitialized.current = true;
|
|
const url = `${getBaseApiReact()}/chat/messages?involving=${selectedDirectAddress}&involving=${myAddress}&encoding=BASE64&limit=0&reverse=false`;
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
const responseData = await response.json();
|
|
decryptMessages(responseData, false);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
const decryptMessages = (encryptedMessages: any[], isInitiated: boolean) => {
|
|
try {
|
|
return new Promise((res, rej) => {
|
|
window
|
|
.sendMessage('decryptDirect', {
|
|
data: encryptedMessages,
|
|
involvingAddress: selectedDirect?.address,
|
|
})
|
|
.then((decryptResponse) => {
|
|
if (!decryptResponse?.error) {
|
|
const response = processWithNewMessages(
|
|
decryptResponse,
|
|
selectedDirect?.address
|
|
);
|
|
res(response);
|
|
|
|
if (isInitiated) {
|
|
const formatted = response
|
|
.filter((rawItem) => !rawItem?.chatReference)
|
|
.map((item) => ({
|
|
...item,
|
|
id: item.signature,
|
|
text: item.message,
|
|
unread: item?.sender === myAddress ? false : true,
|
|
}));
|
|
|
|
setMessages((prev) => [...prev, ...formatted]);
|
|
setChatReferences((prev) => {
|
|
const organizedChatReferences = { ...prev };
|
|
|
|
response
|
|
.filter(
|
|
(rawItem) =>
|
|
!!rawItem?.chatReference && rawItem?.type === 'edit'
|
|
)
|
|
.forEach((item) => {
|
|
try {
|
|
organizedChatReferences[item.chatReference] = {
|
|
...(organizedChatReferences[item.chatReference] ||
|
|
{}),
|
|
edit: item,
|
|
};
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
});
|
|
return organizedChatReferences;
|
|
});
|
|
} else {
|
|
hasInitialized.current = true;
|
|
const formatted = response
|
|
.filter((rawItem) => !rawItem?.chatReference)
|
|
.map((item) => ({
|
|
...item,
|
|
id: item.signature,
|
|
text: item.message,
|
|
unread: false,
|
|
}));
|
|
setMessages(formatted);
|
|
|
|
setChatReferences((prev) => {
|
|
const organizedChatReferences = { ...prev };
|
|
|
|
response
|
|
.filter(
|
|
(rawItem) =>
|
|
!!rawItem?.chatReference && rawItem?.type === 'edit'
|
|
)
|
|
.forEach((item) => {
|
|
try {
|
|
organizedChatReferences[item.chatReference] = {
|
|
...(organizedChatReferences[item.chatReference] ||
|
|
{}),
|
|
edit: item,
|
|
};
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
});
|
|
return organizedChatReferences;
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
rej(response.error);
|
|
})
|
|
.catch((error) => {
|
|
rej(
|
|
error.message ||
|
|
t('core:message.error.generic', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
};
|
|
|
|
const forceCloseWebSocket = () => {
|
|
if (socketRef.current) {
|
|
clearTimeout(timeoutIdRef.current);
|
|
clearTimeout(groupSocketTimeoutRef.current);
|
|
socketRef.current.close(1000, 'forced');
|
|
socketRef.current = null;
|
|
}
|
|
};
|
|
|
|
const pingWebSocket = () => {
|
|
try {
|
|
if (socketRef.current?.readyState === WebSocket.OPEN) {
|
|
socketRef.current.send('ping');
|
|
timeoutIdRef.current = setTimeout(() => {
|
|
if (socketRef.current) {
|
|
socketRef.current.close();
|
|
clearTimeout(groupSocketTimeoutRef.current);
|
|
}
|
|
}, 5000); // Close if no pong in 5 seconds
|
|
}
|
|
} catch (error) {
|
|
console.error('Error during ping:', error);
|
|
}
|
|
};
|
|
|
|
const initWebsocketMessageGroup = () => {
|
|
forceCloseWebSocket(); // Close any existing connection
|
|
|
|
if (!selectedDirect?.address || !myAddress) return;
|
|
|
|
const socketLink = `${getBaseApiReactSocket()}/websockets/chat/messages?involving=${selectedDirect?.address}&involving=${myAddress}&encoding=BASE64&limit=100`;
|
|
socketRef.current = new WebSocket(socketLink);
|
|
|
|
socketRef.current.onopen = () => {
|
|
setTimeout(pingWebSocket, 50); // Initial ping
|
|
};
|
|
|
|
socketRef.current.onmessage = (e) => {
|
|
try {
|
|
if (e.data === 'pong') {
|
|
clearTimeout(timeoutIdRef.current);
|
|
groupSocketTimeoutRef.current = setTimeout(pingWebSocket, 45000); // Ping every 45 seconds
|
|
} else {
|
|
middletierFunc(
|
|
JSON.parse(e.data),
|
|
selectedDirect?.address,
|
|
myAddress
|
|
);
|
|
|
|
setIsLoading(false);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error handling WebSocket message:', error);
|
|
}
|
|
};
|
|
|
|
socketRef.current.onclose = (event) => {
|
|
clearTimeout(groupSocketTimeoutRef.current);
|
|
clearTimeout(timeoutIdRef.current);
|
|
console.warn(`WebSocket closed: ${event.reason || 'unknown reason'}`);
|
|
if (event.reason !== 'forced' && event.code !== 1000) {
|
|
setTimeout(() => initWebsocketMessageGroup(), 10000); // Retry after 10 seconds
|
|
}
|
|
};
|
|
|
|
socketRef.current.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
clearTimeout(groupSocketTimeoutRef.current);
|
|
clearTimeout(timeoutIdRef.current);
|
|
if (socketRef.current) {
|
|
socketRef.current.close();
|
|
}
|
|
};
|
|
};
|
|
|
|
const setDirectChatValueFunc = async (e) => {
|
|
setDirectToValue(e.detail.directToValue);
|
|
};
|
|
useEffect(() => {
|
|
subscribeToEvent('setDirectToValueNewChat', setDirectChatValueFunc);
|
|
|
|
return () => {
|
|
unsubscribeFromEvent('setDirectToValueNewChat', setDirectChatValueFunc);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (hasInitializedWebsocket.current || isNewChat) return;
|
|
setIsLoading(true);
|
|
initWebsocketMessageGroup();
|
|
hasInitializedWebsocket.current = true;
|
|
|
|
return () => {
|
|
forceCloseWebSocket(); // Clean up WebSocket on component unmount
|
|
};
|
|
}, [selectedDirect?.address, myAddress, isNewChat]);
|
|
|
|
const sendChatDirect = async (
|
|
{ chatReference = undefined, messageText, otherData }: any,
|
|
address,
|
|
publicKeyOfRecipient,
|
|
isNewChatVar
|
|
) => {
|
|
try {
|
|
const directTo = isNewChatVar ? directToValue : address;
|
|
|
|
if (!directTo) return;
|
|
return new Promise((res, rej) => {
|
|
window
|
|
.sendMessage(
|
|
'sendChatDirect',
|
|
{
|
|
directTo,
|
|
chatReference,
|
|
messageText,
|
|
otherData,
|
|
publicKeyOfRecipient,
|
|
address: directTo,
|
|
},
|
|
120000
|
|
)
|
|
.then(async (response) => {
|
|
if (!response?.error) {
|
|
if (isNewChatVar) {
|
|
let getRecipientName = null;
|
|
try {
|
|
getRecipientName = await getNameInfo(response.recipient);
|
|
} catch (error) {
|
|
console.error('Error fetching recipient name:', error);
|
|
}
|
|
setSelectedDirect({
|
|
address: response.recipient,
|
|
name: getRecipientName,
|
|
timestamp: Date.now(),
|
|
sender: myAddress,
|
|
senderName: myName,
|
|
});
|
|
setNewChat(null);
|
|
window
|
|
.sendMessage('addTimestampEnterChat', {
|
|
timestamp: Date.now(),
|
|
groupId: response.recipient,
|
|
})
|
|
.catch((error) => {
|
|
console.error(
|
|
'Failed to add timestamp:',
|
|
error.message || 'An error occurred'
|
|
);
|
|
});
|
|
|
|
setTimeout(() => {
|
|
getTimestampEnterChat();
|
|
}, 400);
|
|
}
|
|
res(response);
|
|
return;
|
|
}
|
|
rej(response.error);
|
|
})
|
|
.catch((error) => {
|
|
rej(
|
|
error.message ||
|
|
t('core:message.error.generic', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
});
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
throw new Error(error.message);
|
|
} else {
|
|
throw new Error(String(error));
|
|
}
|
|
}
|
|
};
|
|
const clearEditorContent = () => {
|
|
if (editorRef.current) {
|
|
setMessageSize(0);
|
|
editorRef.current.chain().focus().clearContent().run();
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
if (!editorRef?.current) return;
|
|
const handleUpdate = () => {
|
|
const htmlContent = editorRef?.current.getHTML();
|
|
const stringified = JSON.stringify(htmlContent);
|
|
const size = new Blob([stringified]).size;
|
|
setMessageSize(size + 200);
|
|
};
|
|
|
|
// Add a listener for the editorRef?.current's content updates
|
|
editorRef?.current.on('update', handleUpdate);
|
|
|
|
// Cleanup the listener on unmount
|
|
return () => {
|
|
editorRef?.current.off('update', handleUpdate);
|
|
};
|
|
}, [editorRef?.current]);
|
|
|
|
const sendMessage = async () => {
|
|
try {
|
|
if (messageSize > 4000) return;
|
|
|
|
// TODO set magic number in a proper file
|
|
if (+balance < 4)
|
|
throw new Error(
|
|
t('group:message.error.qortals_required', {
|
|
quantity: 4,
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
);
|
|
if (isSending) return;
|
|
if (editorRef.current) {
|
|
const htmlContent = editorRef.current.getHTML();
|
|
|
|
if (!htmlContent?.trim() || htmlContent?.trim() === '<p></p>') return;
|
|
setIsSending(true);
|
|
pauseAllQueues();
|
|
const message = JSON.stringify(htmlContent);
|
|
|
|
if (isNewChat) {
|
|
await sendChatDirect({ messageText: htmlContent }, null, null, true);
|
|
return;
|
|
}
|
|
let repliedTo = replyMessage?.signature;
|
|
|
|
if (replyMessage?.chatReference) {
|
|
repliedTo = replyMessage?.chatReference;
|
|
}
|
|
let chatReference = onEditMessage?.signature;
|
|
|
|
const otherData = {
|
|
...(onEditMessage?.decryptedData || {}),
|
|
specialId: uid.rnd(),
|
|
repliedTo: onEditMessage ? onEditMessage?.repliedTo : repliedTo,
|
|
type: chatReference ? 'edit' : '',
|
|
};
|
|
const sendMessageFunc = async () => {
|
|
return await sendChatDirect(
|
|
{ chatReference, messageText: htmlContent, otherData },
|
|
selectedDirect?.address,
|
|
publicKeyOfRecipient,
|
|
false
|
|
);
|
|
};
|
|
|
|
// Add the function to the queue
|
|
const messageObj = {
|
|
message: {
|
|
timestamp: Date.now(),
|
|
senderName: myName,
|
|
sender: myAddress,
|
|
...(otherData || {}),
|
|
text: htmlContent,
|
|
},
|
|
chatReference,
|
|
};
|
|
addToQueue(
|
|
sendMessageFunc,
|
|
messageObj,
|
|
'chat-direct',
|
|
selectedDirect?.address
|
|
);
|
|
setTimeout(() => {
|
|
executeEvent('sent-new-message-group', {});
|
|
}, 150);
|
|
clearEditorContent();
|
|
setReplyMessage(null);
|
|
setOnEditMessage(null);
|
|
}
|
|
// send chat message
|
|
} catch (error) {
|
|
const errorMsg = error?.message || error;
|
|
setInfoSnack({
|
|
type: 'error',
|
|
message:
|
|
errorMsg === 'invalid signature'
|
|
? t('group:message.error.qortals_required', {
|
|
quantity: 4,
|
|
postProcess: 'capitalizeFirstChar',
|
|
})
|
|
: errorMsg,
|
|
});
|
|
setOpenSnack(true);
|
|
console.error(error);
|
|
} finally {
|
|
setIsSending(false);
|
|
resumeAllQueues();
|
|
}
|
|
};
|
|
|
|
const onReply = useCallback(
|
|
(message) => {
|
|
if (onEditMessage) {
|
|
clearEditorContent();
|
|
}
|
|
setReplyMessage(message);
|
|
setOnEditMessage(null);
|
|
editorRef?.current?.chain().focus();
|
|
},
|
|
[onEditMessage]
|
|
);
|
|
|
|
const onEdit = useCallback((message) => {
|
|
setOnEditMessage(message);
|
|
setReplyMessage(null);
|
|
editorRef.current.chain().focus().setContent(message?.text).run();
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
background: theme.palette.background.default,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
height: '100vh',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<Box
|
|
onClick={close}
|
|
sx={{
|
|
alignItems: 'center',
|
|
alignSelf: 'center',
|
|
background: theme.palette.background.default,
|
|
borderRadius: '3px',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
gap: '5px',
|
|
margin: '10px 0px',
|
|
padding: '4px 6px',
|
|
width: 'fit-content',
|
|
}}
|
|
>
|
|
<ArrowBackIcon
|
|
sx={{
|
|
color: theme.palette.text.primary,
|
|
fontSize: '20px',
|
|
}}
|
|
/>
|
|
<Typography
|
|
sx={{
|
|
color: theme.palette.text.primary,
|
|
fontSize: '14px',
|
|
}}
|
|
>
|
|
{t('core:action.close_chat', { postProcess: 'capitalizeFirstChar' })}
|
|
</Typography>
|
|
</Box>
|
|
|
|
{isNewChat && (
|
|
<>
|
|
<Spacer height="30px" />
|
|
|
|
<Input
|
|
sx={{
|
|
fontSize: '18px',
|
|
padding: '5px',
|
|
}}
|
|
placeholder={t('auth:message.generic.name_address', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
value={directToValue}
|
|
onChange={(e) => setDirectToValue(e.target.value)}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<ChatList
|
|
chatReferences={chatReferences}
|
|
onEdit={onEdit}
|
|
onReply={onReply}
|
|
chatId={selectedDirect?.address}
|
|
initialMessages={messages}
|
|
myAddress={myAddress}
|
|
tempMessages={tempMessages}
|
|
tempChatReferences={tempChatReferences}
|
|
/>
|
|
|
|
<div
|
|
style={{
|
|
backgroundColor: theme.palette.background.default,
|
|
bottom: isFocusedParent ? '0px' : 'unset',
|
|
boxSizing: 'border-box',
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
flexShrink: 0,
|
|
minHeight: '150px',
|
|
overflow: 'hidden',
|
|
padding: '20px',
|
|
position: isFocusedParent ? 'fixed' : 'relative',
|
|
top: isFocusedParent ? '0px' : 'unset',
|
|
width: '100%',
|
|
zIndex: isFocusedParent ? 5 : 'unset',
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
flexGrow: 1,
|
|
flexShrink: 0,
|
|
justifyContent: 'flex-end',
|
|
overflow: 'auto',
|
|
width: 'calc(100% - 100px)',
|
|
}}
|
|
>
|
|
{replyMessage && (
|
|
<Box
|
|
sx={{
|
|
alignItems: 'flex-start',
|
|
display: 'flex',
|
|
gap: '5px',
|
|
justifyContent: 'flex-end',
|
|
width: 'calc(100% - 100px)',
|
|
}}
|
|
>
|
|
<ReplyPreview message={replyMessage} />
|
|
|
|
<ButtonBase
|
|
onClick={() => {
|
|
setReplyMessage(null);
|
|
setOnEditMessage(null);
|
|
}}
|
|
>
|
|
<ExitIcon />
|
|
</ButtonBase>
|
|
</Box>
|
|
)}
|
|
{onEditMessage && (
|
|
<Box
|
|
sx={{
|
|
alignItems: 'flex-start',
|
|
display: 'flex',
|
|
gap: '5px',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<ReplyPreview isEdit message={onEditMessage} />
|
|
|
|
<ButtonBase
|
|
onClick={() => {
|
|
setReplyMessage(null);
|
|
setOnEditMessage(null);
|
|
clearEditorContent();
|
|
}}
|
|
>
|
|
<ExitIcon />
|
|
</ButtonBase>
|
|
</Box>
|
|
)}
|
|
|
|
<Tiptap
|
|
isFocusedParent={isFocusedParent}
|
|
setEditorRef={setEditorRef}
|
|
onEnter={sendMessage}
|
|
isChat
|
|
disableEnter={false}
|
|
setIsFocusedParent={setIsFocusedParent}
|
|
/>
|
|
{messageSize > 750 && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
justifyContent: 'flex-start',
|
|
position: 'relative',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<Typography // TODO set magic number in a proper file
|
|
sx={{
|
|
fontSize: '12px',
|
|
color:
|
|
messageSize > 4000 ? theme.palette.other.danger : 'unset',
|
|
}}
|
|
>
|
|
{t('core:message.error.message_size', {
|
|
maximum: 4000,
|
|
size: messageSize,
|
|
postProcess: 'capitalizeFirstChar',
|
|
})}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</div>
|
|
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
flexShrink: 0,
|
|
gap: '10px',
|
|
justifyContent: 'center',
|
|
position: 'relative',
|
|
width: '100px',
|
|
}}
|
|
>
|
|
<CustomButton
|
|
onClick={() => {
|
|
if (isSending) return;
|
|
sendMessage();
|
|
}}
|
|
style={{
|
|
alignSelf: 'center',
|
|
background: isSending
|
|
? theme.palette.background.default
|
|
: theme.palette.background.paper,
|
|
cursor: isSending ? 'default' : 'pointer',
|
|
flexShrink: 0,
|
|
marginTop: 'auto',
|
|
minWidth: 'auto',
|
|
padding: '5px',
|
|
width: '100px',
|
|
}}
|
|
>
|
|
{isSending && (
|
|
<CircularProgress
|
|
size={18}
|
|
sx={{
|
|
color: theme.palette.text.primary,
|
|
left: '50%',
|
|
marginLeft: '-12px',
|
|
marginTop: '-12px',
|
|
position: 'absolute',
|
|
top: '50%',
|
|
}}
|
|
/>
|
|
)}
|
|
{` Send`}
|
|
</CustomButton>
|
|
</Box>
|
|
</div>
|
|
|
|
<LoadingSnackbar
|
|
open={isLoading}
|
|
info={{
|
|
message: t('core:loading.chat', {
|
|
postProcess: 'capitalizeFirstChar',
|
|
}),
|
|
}}
|
|
/>
|
|
|
|
<CustomizedSnackbars
|
|
open={openSnack}
|
|
setOpen={setOpenSnack}
|
|
info={infoSnack}
|
|
setInfo={setInfoSnack}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|