mirror of
https://github.com/Qortal/chrome-extension.git
synced 2025-08-01 14:41:26 +00:00
version 2 - beta
This commit is contained in:
554
src/components/Group/Forum/NewThread.tsx
Normal file
554
src/components/Group/Forum/NewThread.tsx
Normal file
@@ -0,0 +1,554 @@
|
||||
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, 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
|
||||
}: 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("");
|
||||
};
|
||||
|
||||
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();
|
||||
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: "15px",
|
||||
}}
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<ComposeIcon src={ComposeIconSVG} />
|
||||
<ComposeP>{currentThread ? "New Post" : "New Thread"}</ComposeP>
|
||||
</ComposeContainer>
|
||||
|
||||
<ReusableModal
|
||||
open={isOpen}
|
||||
customStyles={{
|
||||
maxHeight: "95vh",
|
||||
maxWidth: "950px",
|
||||
height: "700px",
|
||||
borderRadius: "12px 12px 0px 0px",
|
||||
background: "#434448",
|
||||
padding: "0px",
|
||||
gap: "0px",
|
||||
}}
|
||||
>
|
||||
<InstanceListHeader
|
||||
sx={{
|
||||
height: "50px",
|
||||
padding: "20px 42px",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#434448",
|
||||
}}
|
||||
>
|
||||
<NewMessageHeaderP>
|
||||
{isMessage ? "Post Message" : "New Thread"}
|
||||
</NewMessageHeaderP>
|
||||
<CloseContainer onClick={closeModal}>
|
||||
<NewMessageCloseImg src={ModalCloseSVG} />
|
||||
</CloseContainer>
|
||||
</InstanceListHeader>
|
||||
<InstanceListContainer
|
||||
sx={{
|
||||
backgroundColor: "#434448",
|
||||
padding: "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: "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>
|
||||
)}
|
||||
<Spacer height="30px" />
|
||||
<Box
|
||||
sx={{
|
||||
maxHeight: "40vh",
|
||||
}}
|
||||
>
|
||||
<TipTap
|
||||
setEditorRef={setEditorRef}
|
||||
onEnter={sendMail}
|
||||
disableEnter
|
||||
/>
|
||||
{/* <TextEditor
|
||||
inlineContent={value}
|
||||
setInlineContent={(val: any) => {
|
||||
setValue(val);
|
||||
}}
|
||||
/> */}
|
||||
</Box>
|
||||
</InstanceListContainer>
|
||||
<InstanceFooter
|
||||
sx={{
|
||||
backgroundColor: "#434448",
|
||||
padding: "20px 42px",
|
||||
alignItems: "center",
|
||||
height: "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>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user