2024-09-14 19:51:31 +03:00

565 lines
16 KiB
TypeScript

import React, { useEffect, useRef, useState } from "react";
import { Box, Button, CircularProgress, Input, Typography } from "@mui/material";
import ShortUniqueId from "short-unique-id";
import CloseIcon from "@mui/icons-material/Close";
import ModalCloseSVG from "../../../assets/svgs/ModalClose.svg";
import ComposeIconSVG from "../../../assets/svgs/ComposeIcon.svg";
import {
AttachmentContainer,
CloseContainer,
ComposeContainer,
ComposeIcon,
ComposeP,
InstanceFooter,
InstanceListContainer,
InstanceListHeader,
NewMessageAttachmentImg,
NewMessageCloseImg,
NewMessageHeaderP,
NewMessageInputRow,
NewMessageSendButton,
NewMessageSendP,
} from "./Mail-styles";
import { ReusableModal } from "./ReusableModal";
import { Spacer } from "../../../common/Spacer";
import { formatBytes } from "../../../utils/Size";
import { CreateThreadIcon } from "../../../assets/svgs/CreateThreadIcon";
import { SendNewMessage } from "../../../assets/svgs/SendNewMessage";
import { TextEditor } from "./TextEditor";
import { MyContext, isMobile, pauseAllQueues, resumeAllQueues } from "../../../App";
import { getFee } from "../../../background";
import TipTap from "../../Chat/TipTap";
import { MessageDisplay } from "../../Chat/MessageDisplay";
import { CustomizedSnackbars } from "../../Snackbar/Snackbar";
import { saveTempPublish } from "../../Chat/GroupAnnouncements";
const uid = new ShortUniqueId({ length: 8 });
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => {
reject(error);
};
});
export function objectToBase64(obj: any) {
// Step 1: Convert the object to a JSON string
const jsonString = JSON.stringify(obj);
// Step 2: Create a Blob from the JSON string
const blob = new Blob([jsonString], { type: "application/json" });
// Step 3: Create a FileReader to read the Blob as a base64-encoded string
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
if (typeof reader.result === "string") {
// Remove 'data:application/json;base64,' prefix
const base64 = reader.result.replace(
"data:application/json;base64,",
""
);
resolve(base64);
} else {
reject(new Error("Failed to read the Blob as a base64-encoded string"));
}
};
reader.onerror = () => {
reject(reader.error);
};
reader.readAsDataURL(blob);
});
}
interface NewMessageProps {
hideButton?: boolean;
groupInfo: any;
currentThread?: any;
isMessage?: boolean;
messageCallback?: (val: any) => void;
publishCallback?: () => void;
refreshLatestThreads?: () => void;
members: any;
}
export const publishGroupEncryptedResource = async ({
encryptedData,
identifier,
}) => {
return new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "publishGroupEncryptedResource",
payload: {
encryptedData,
identifier,
},
},
(response) => {
if (!response?.error) {
res(response);
return
}
rej(response.error);
}
);
});
};
export const encryptSingleFunc = async (data: string, secretKeyObject: any) => {
try {
return new Promise((res, rej) => {
chrome?.runtime?.sendMessage(
{
action: "encryptSingle",
payload: {
data,
secretKeyObject,
},
},
(response) => {
if (!response?.error) {
res(response);
return;
}
rej(response.error);
}
);
});
} catch (error) {}
};
export const NewThread = ({
groupInfo,
members,
currentThread,
isMessage = false,
publishCallback,
userInfo,
getSecretKey,
closeCallback,
postReply,
myName,
setPostReply
}: NewMessageProps) => {
const { show } = React.useContext(MyContext);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [value, setValue] = useState("");
const [isSending, setIsSending] = useState(false);
const [threadTitle, setThreadTitle] = useState<string>("");
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
const editorRef = useRef(null);
const setEditorRef = (editorInstance) => {
editorRef.current = editorInstance;
};
useEffect(() => {
if (postReply) {
setIsOpen(true);
}
}, [postReply]);
const closeModal = () => {
setIsOpen(false);
setValue("");
setPostReply(null)
};
async function publishQDNResource() {
try {
pauseAllQueues()
if(isSending) return
setIsSending(true)
let name: string = "";
let errorMsg = "";
name = userInfo?.name || "";
const missingFields: string[] = [];
if (!isMessage && !threadTitle) {
errorMsg = "Please provide a thread title";
}
if (!name) {
errorMsg = "Cannot send a message without a access to your name";
}
if (!groupInfo) {
errorMsg = "Cannot access group information";
}
// if (!description) missingFields.push('subject')
if (missingFields.length > 0) {
const missingFieldsString = missingFields.join(", ");
const errMsg = `Missing: ${missingFieldsString}`;
errorMsg = errMsg;
}
if (errorMsg) {
// dispatch(
// setNotification({
// msg: errorMsg,
// alertType: "error",
// })
// );
throw new Error(errorMsg);
}
const htmlContent = editorRef.current.getHTML();
if (!htmlContent?.trim() || htmlContent?.trim() === "<p></p>")
throw new Error("Please provide a first message to the thread");
const fee = await getFee("ARBITRARY");
let feeToShow = fee.fee;
if (!isMessage) {
feeToShow = +feeToShow * 2;
}
await show({
message: "Would you like to perform a ARBITRARY transaction?",
publishFee: feeToShow + " QORT",
});
let reply = null;
if (postReply) {
reply = { ...postReply };
if (reply.reply) {
delete reply.reply;
}
}
const mailObject: any = {
createdAt: Date.now(),
version: 1,
textContentV2: htmlContent,
name,
threadOwner: currentThread?.threadData?.name || name,
reply,
};
const secretKey = await getSecretKey(false, true);
if (!secretKey) {
throw new Error("Cannot get group secret key");
}
if (!isMessage) {
const idThread = uid.rnd();
const idMsg = uid.rnd();
const messageToBase64 = await objectToBase64(mailObject);
const encryptSingleFirstPost = await encryptSingleFunc(
messageToBase64,
secretKey
);
const threadObject = {
title: threadTitle,
groupId: groupInfo.id,
createdAt: Date.now(),
name,
};
const threadToBase64 = await objectToBase64(threadObject);
const encryptSingleThread = await encryptSingleFunc(
threadToBase64,
secretKey
);
let identifierThread = `grp-${groupInfo.groupId}-thread-${idThread}`;
await publishGroupEncryptedResource({
identifier: identifierThread,
encryptedData: encryptSingleThread,
});
let identifierPost = `thmsg-${identifierThread}-${idMsg}`;
await publishGroupEncryptedResource({
identifier: identifierPost,
encryptedData: encryptSingleFirstPost,
});
const dataToSaveToStorage = {
name: myName,
identifier: identifierThread,
service: 'DOCUMENT',
tempData: threadObject,
created: Date.now(),
}
const dataToSaveToStoragePost = {
name: myName,
identifier: identifierPost,
service: 'DOCUMENT',
tempData: mailObject,
created: Date.now(),
threadId: identifierThread
}
await saveTempPublish({data: dataToSaveToStorage, key: 'thread'})
await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'})
setInfoSnack({
type: "success",
message: "Successfully created thread. It may take some time for the publish to propagate",
});
setOpenSnack(true)
// dispatch(
// setNotification({
// msg: "Message sent",
// alertType: "success",
// })
// );
if (publishCallback) {
publishCallback()
// threadCallback({
// threadData: threadObject,
// threadOwner: name,
// name,
// threadId: identifierThread,
// created: Date.now(),
// service: 'MAIL_PRIVATE',
// identifier: identifier
// })
}
closeModal();
} else {
if (!currentThread) throw new Error("unable to locate thread Id");
const idThread = currentThread.threadId;
const messageToBase64 = await objectToBase64(mailObject);
const encryptSinglePost = await encryptSingleFunc(
messageToBase64,
secretKey
);
const idMsg = uid.rnd();
let identifier = `thmsg-${idThread}-${idMsg}`;
const res = await publishGroupEncryptedResource({
identifier: identifier,
encryptedData: encryptSinglePost,
});
const dataToSaveToStoragePost = {
threadId: idThread,
name: myName,
identifier: identifier,
service: 'DOCUMENT',
tempData: mailObject,
created: Date.now()
}
await saveTempPublish({data: dataToSaveToStoragePost, key: 'thread-post'})
// await qortalRequest(multiplePublishMsg);
// dispatch(
// setNotification({
// msg: "Message sent",
// alertType: "success",
// })
// );
setInfoSnack({
type: "success",
message: "Successfully created post. It may take some time for the publish to propagate",
});
setOpenSnack(true)
if(publishCallback){
publishCallback()
}
// messageCallback({
// identifier,
// id: identifier,
// name,
// service: MAIL_SERVICE_TYPE,
// created: Date.now(),
// ...mailObject,
// });
}
closeModal();
} catch (error: any) {
if(error?.message){
setInfoSnack({
type: "error",
message: error?.message,
});
setOpenSnack(true)
}
} finally {
setIsSending(false);
resumeAllQueues()
}
}
const sendMail = () => {
publishQDNResource();
};
return (
<Box
sx={{
display: "flex",
}}
>
<ComposeContainer
sx={{
padding: isMobile ? '5px' : "15px",
justifyContent: isMobile ? 'flex-start' : 'revert'
}}
onClick={() => setIsOpen(true)}
>
<ComposeIcon src={ComposeIconSVG} />
<ComposeP>{currentThread ? "New Post" : "New Thread"}</ComposeP>
</ComposeContainer>
<ReusableModal
open={isOpen}
customStyles={{
maxHeight: isMobile ? '95svh' : "95vh",
maxWidth: "950px",
height: "700px",
borderRadius: "12px 12px 0px 0px",
background: "#434448",
padding: "0px",
gap: "0px",
}}
>
<InstanceListHeader
sx={{
height: isMobile ? 'auto' : "50px",
padding: isMobile ? '5px' : "20px 42px",
flexDirection: "row",
alignItems: 'center',
justifyContent: "space-between",
backgroundColor: "#434448",
}}
>
<NewMessageHeaderP>
{isMessage ? "Post Message" : "New Thread"}
</NewMessageHeaderP>
<CloseContainer sx={{
height: '40px'
}} onClick={closeModal}>
<NewMessageCloseImg src={ModalCloseSVG} />
</CloseContainer>
</InstanceListHeader>
<InstanceListContainer
sx={{
backgroundColor: "#434448",
padding: isMobile ? '5px' : "20px 42px",
height: "calc(100% - 150px)",
flexShrink: 0,
}}
>
{!isMessage && (
<>
<Spacer height="10px" />
<NewMessageInputRow>
<Input
id="standard-adornment-name"
value={threadTitle}
onChange={(e) => {
setThreadTitle(e.target.value);
}}
placeholder="Thread Title"
disableUnderline
autoComplete="off"
autoCorrect="off"
sx={{
width: "100%",
color: "white",
"& .MuiInput-input::placeholder": {
color: "rgba(255,255,255, 0.70) !important",
fontSize: isMobile ? '14px' : "20px",
fontStyle: "normal",
fontWeight: 400,
lineHeight: "120%", // 24px
letterSpacing: "0.15px",
opacity: 1,
},
"&:focus": {
outline: "none",
},
// Add any additional styles for the input here
}}
/>
</NewMessageInputRow>
</>
)}
{postReply && postReply.textContentV2 && (
<Box
sx={{
width: "100%",
maxHeight: "120px",
overflow: "auto",
}}
>
<MessageDisplay htmlContent={postReply?.textContentV2} />
</Box>
)}
{!isMobile && (
<Spacer height="30px" />
)}
<Box
sx={{
maxHeight: "40vh",
}}
>
<TipTap
setEditorRef={setEditorRef}
onEnter={sendMail}
disableEnter
overrideMobile
customEditorHeight="240px"
/>
{/* <TextEditor
inlineContent={value}
setInlineContent={(val: any) => {
setValue(val);
}}
/> */}
</Box>
</InstanceListContainer>
<InstanceFooter
sx={{
backgroundColor: "#434448",
padding: isMobile ? '5px' : "20px 42px",
alignItems: "center",
height: isMobile ? 'auto' : "90px",
}}
>
<NewMessageSendButton onClick={sendMail}>
{isSending && (
<Box sx={{height: '100%', position: 'absolute', width: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<CircularProgress sx={{
}} size={'12px'} />
</Box>
)}
<NewMessageSendP>
{isMessage ? "Post" : "Create Thread"}
</NewMessageSendP>
{isMessage ? (
<SendNewMessage
color="red"
opacity={1}
height="25px"
width="25px"
/>
) : (
<CreateThreadIcon
color="red"
opacity={1}
height="25px"
width="25px"
/>
)}
</NewMessageSendButton>
</InstanceFooter>
</ReusableModal>
<CustomizedSnackbars open={openSnack} setOpen={setOpenSnack} info={infoSnack} setInfo={setInfoSnack} />
</Box>
);
};