import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { CreateCommonSecret } from "./CreateCommonSecret"; import { reusableGet } from "../../qdn/publish/pubish"; import { uint8ArrayToObject } from "../../backgroundFunctions/encryption"; import { base64ToUint8Array, objectToBase64, } from "../../qdn/encryption/group-encryption"; import { ChatContainerComp } from "./ChatContainer"; import { ChatList } from "./ChatList"; import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; import Tiptap from "./TipTap"; import { AuthenticatedContainerInnerTop, CustomButton } from "../../App-styles"; import CircularProgress from "@mui/material/CircularProgress"; import { getBaseApi, getFee } from "../../background"; import { LoadingSnackbar } from "../Snackbar/LoadingSnackbar"; import { Box, Typography } from "@mui/material"; import { Spacer } from "../../common/Spacer"; import ShortUniqueId from "short-unique-id"; import { AnnouncementList } from "./AnnouncementList"; const uid = new ShortUniqueId({ length: 8 }); import CampaignIcon from "@mui/icons-material/Campaign"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import { AnnouncementDiscussion } from "./AnnouncementDiscussion"; import { MyContext, getArbitraryEndpointReact, getBaseApiReact, isMobile, pauseAllQueues, resumeAllQueues, } from "../../App"; import { RequestQueueWithPromise } from "../../utils/queue/queue"; import { CustomizedSnackbars } from "../Snackbar/Snackbar"; import { addDataPublishesFunc, getDataPublishesFunc } from "../Group/Group"; import { getRootHeight } from "../../utils/mobile/mobileUtils"; export const requestQueueCommentCount = new RequestQueueWithPromise(3); export const requestQueuePublishedAccouncements = new RequestQueueWithPromise( 3 ); export const saveTempPublish = async ({ data, key }: any) => { return new Promise((res, rej) => { window.sendMessage("saveTempPublish", { data, key, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || "An error occurred"); }); }); }; export const getTempPublish = async () => { return new Promise((res, rej) => { window.sendMessage("getTempPublish", {}) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || "An error occurred"); }); }); }; export const decryptPublishes = async (encryptedMessages: any[], secretKey) => { try { return await new Promise((res, rej) => { chrome?.runtime?.sendMessage( { action: "decryptSingleForPublishes", payload: { data: encryptedMessages, secretKeyObject: secretKey, skipDecodeBase64: true, }, }, (response) => { if (!response?.error) { res(response); // if(hasInitialized.current){ // setMessages((prev)=> [...prev, ...formatted]) // } else { // const formatted = response.map((item: any)=> { // return { // ...item, // id: item.signature, // text: item.text, // unread: false // } // } ) // setMessages(formatted) // hasInitialized.current = true // } } rej(response.error); } ); }); } catch (error) {} }; export const GroupAnnouncements = ({ selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, isAdmin, hide, myName, }) => { const [messages, setMessages] = useState([]); const [isSending, setIsSending] = useState(false); const [isLoading, setIsLoading] = useState(true); const [announcements, setAnnouncements] = useState([]); const [tempPublishedList, setTempPublishedList] = useState([]); const [announcementData, setAnnouncementData] = useState({}); const [selectedAnnouncement, setSelectedAnnouncement] = useState(null); const [isFocusedParent, setIsFocusedParent] = useState(false); const { show, rootHeight } = React.useContext(MyContext); const [openSnack, setOpenSnack] = React.useState(false); const [infoSnack, setInfoSnack] = React.useState(null); const hasInitialized = useRef(false); const hasInitializedWebsocket = useRef(false); const editorRef = useRef(null); const dataPublishes = useRef({}); const setEditorRef = (editorInstance) => { editorRef.current = editorInstance; }; const [, forceUpdate] = React.useReducer((x) => x + 1, 0); const triggerRerender = () => { forceUpdate(); // Trigger re-render by updating the state }; useEffect(() => { if (!selectedGroup) return; (async () => { const res = await getDataPublishesFunc(selectedGroup, "anc"); dataPublishes.current = res || {}; })(); }, [selectedGroup]); const getAnnouncementData = async ({ identifier, name, resource }) => { try { let data = dataPublishes.current[`${name}-${identifier}`]; if ( !data || data?.update || data?.created !== (resource?.updated || resource?.created) ) { const res = await requestQueuePublishedAccouncements.enqueue(() => { return fetch( `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` ); }); if (!res?.ok) return; data = await res.text(); await addDataPublishesFunc({ ...resource, data }, selectedGroup, "anc"); } else { data = data.data; } const response = await decryptPublishes([{ data }], secretKey); const messageData = response[0]; setAnnouncementData((prev) => { return { ...prev, [`${identifier}-${name}`]: messageData, }; }); } catch (error) { console.log("error", error); } }; useEffect(() => { if (!secretKey || hasInitializedWebsocket.current) return; setIsLoading(true); // initWebsocketMessageGroup() hasInitializedWebsocket.current = true; }, [secretKey]); const encryptChatMessage = 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) {} }; const publishAnc = async ({ encryptedData, identifier }: any) => { return new Promise((res, rej) => { chrome?.runtime?.sendMessage( { action: "publishGroupEncryptedResource", payload: { encryptedData, identifier, }, }, (response) => { if (!response?.error) { res(response); } rej(response.error); } ); }); }; const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); if (isMobile) { setTimeout(() => { editorRef.current?.chain().blur().run(); setIsFocusedParent(false); setTimeout(() => { triggerRerender(); }, 300); }, 200); } } }; const setTempData = async () => { try { const getTempAnnouncements = await getTempPublish(); if (getTempAnnouncements?.announcement) { let tempData = []; Object.keys(getTempAnnouncements?.announcement || {}).map((key) => { const value = getTempAnnouncements?.announcement[key]; tempData.push(value.data); }); setTempPublishedList(tempData); } } catch (error) {} }; const publishAnnouncement = async () => { try { pauseAllQueues(); const fee = await getFee("ARBITRARY"); await show({ message: "Would you like to perform a ARBITRARY transaction?", publishFee: fee.fee + " QORT", }); if (isSending) return; if (editorRef.current) { const htmlContent = editorRef.current.getHTML(); if (!htmlContent?.trim() || htmlContent?.trim() === "

") return; setIsSending(true); const message = { version: 1, extra: {}, message: htmlContent, }; const secretKeyObject = await getSecretKey(false, true); const message64: any = await objectToBase64(message); const encryptSingle = await encryptChatMessage( message64, secretKeyObject ); const randomUid = uid.rnd(); const identifier = `grp-${selectedGroup}-anc-${randomUid}`; const res = await publishAnc({ encryptedData: encryptSingle, identifier, }); const dataToSaveToStorage = { name: myName, identifier, service: "DOCUMENT", tempData: message, created: Date.now(), }; await saveTempPublish({ data: dataToSaveToStorage, key: "announcement", }); setTempData(); clearEditorContent(); } // send chat message } catch (error) { if (!error) return; setInfoSnack({ type: "error", message: error, }); setOpenSnack(true); } finally { resumeAllQueues(); setIsSending(false); } }; const getAnnouncements = React.useCallback( async (selectedGroup) => { try { const offset = 0; // dispatch(setIsLoadingGlobal(true)) const identifier = `grp-${selectedGroup}-anc-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, }); const responseData = await response.json(); setTempData(); setAnnouncements(responseData); setIsLoading(false); for (const data of responseData) { getAnnouncementData({ name: data.name, identifier: data.identifier, resource: data, }); } } catch (error) { } finally { // dispatch(setIsLoadingGlobal(false)) } }, [secretKey] ); React.useEffect(() => { if (selectedGroup && secretKey && !hasInitialized.current && !hide) { getAnnouncements(selectedGroup); hasInitialized.current = true; } }, [selectedGroup, secretKey, hide]); const loadMore = async () => { try { setIsLoading(true); const offset = announcements.length; const identifier = `grp-${selectedGroup}-anc-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${offset}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, }); const responseData = await response.json(); setAnnouncements((prev) => [...prev, ...responseData]); setIsLoading(false); for (const data of responseData) { getAnnouncementData({ name: data.name, identifier: data.identifier }); } } catch (error) {} }; const interval = useRef(null); const checkNewMessages = React.useCallback(async () => { try { const identifier = `grp-${selectedGroup}-anc-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=20&includemetadata=false&offset=${0}&reverse=true&prefix=true`; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, }); const responseData = await response.json(); const latestMessage = announcements[0]; if (!latestMessage) { for (const data of responseData) { try { getAnnouncementData({ name: data.name, identifier: data.identifier, }); } catch (error) {} } setAnnouncements(responseData); return; } const findMessage = responseData?.findIndex( (item: any) => item?.identifier === latestMessage?.identifier ); if (findMessage === -1) return; const newArray = responseData.slice(0, findMessage); for (const data of newArray) { try { getAnnouncementData({ name: data.name, identifier: data.identifier }); } catch (error) {} } setAnnouncements((prev) => [...newArray, ...prev]); } catch (error) { } finally { } }, [announcements, secretKey, selectedGroup]); const checkNewMessagesFunc = useCallback(() => { let isCalling = false; interval.current = setInterval(async () => { if (isCalling) return; isCalling = true; const res = await checkNewMessages(); isCalling = false; }, 20000); }, [checkNewMessages]); useEffect(() => { if (!secretKey || hide) return; checkNewMessagesFunc(); return () => { if (interval?.current) { clearInterval(interval.current); } }; }, [checkNewMessagesFunc, hide]); const combinedListTempAndReal = useMemo(() => { // Combine the two lists const combined = [...tempPublishedList, ...announcements]; // Remove duplicates based on the "identifier" const uniqueItems = new Map(); combined.forEach((item) => { uniqueItems.set(item.identifier, item); // This will overwrite duplicates, keeping the last occurrence }); // Convert the map back to an array and sort by "created" timestamp in descending order const sortedList = Array.from(uniqueItems.values()).sort( (a, b) => b.created - a.created ); return sortedList; }, [tempPublishedList, announcements]); if (selectedAnnouncement) { return (
); } return (
{!isMobile && ( Group Announcements )}
{!isLoading && combinedListTempAndReal?.length === 0 && ( No announcements )} 0 && announcements.length % 20 === 0 } loadMore={loadMore} myName={myName} /> {isAdmin && (
{isFocusedParent && ( { if (isSending) return; setIsFocusedParent(false); clearEditorContent(); setTimeout(() => { triggerRerender(); }, 300); // Unfocus the editor }} style={{ marginTop: "auto", alignSelf: "center", cursor: isSending ? "default" : "pointer", background: "red", flexShrink: 0, padding: isMobile && "5px", fontSize: isMobile && "14px", }} > {` Close`} )} { if (isSending) return; publishAnnouncement(); }} style={{ marginTop: "auto", alignSelf: "center", cursor: isSending ? "default" : "pointer", background: isSending && "rgba(0, 0, 0, 0.8)", flexShrink: 0, padding: isMobile && "5px", fontSize: isMobile && "14px", }} > {isSending && ( )} {` Publish Announcement`}
)}
); };