import { useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState, } from 'react'; import { uint8ArrayToObject } from '../../backgroundFunctions/encryption'; import { base64ToUint8Array, objectToBase64, } from '../../qdn/encryption/group-encryption'; import Tiptap from './TipTap'; import { CustomButton } from '../../styles/App-styles'; import CircularProgress from '@mui/material/CircularProgress'; import { getFee } from '../../background'; import { LoadingSnackbar } from '../Snackbar/LoadingSnackbar'; import { Box, Typography, useTheme } from '@mui/material'; import { Spacer } from '../../common/Spacer'; import ShortUniqueId from 'short-unique-id'; import { AnnouncementList } from './AnnouncementList'; import CampaignIcon from '@mui/icons-material/Campaign'; import { AnnouncementDiscussion } from './AnnouncementDiscussion'; import { MyContext, getArbitraryEndpointReact, getBaseApiReact, pauseAllQueues, resumeAllQueues, } from '../../App'; import { RequestQueueWithPromise } from '../../utils/queue/queue'; import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { addDataPublishesFunc, getDataPublishesFunc } from '../Group/Group'; import { useTranslation } from 'react-i18next'; const uid = new ShortUniqueId({ length: 8 }); 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) => { window .sendMessage('decryptSingleForPublishes', { data: encryptedMessages, secretKeyObject: secretKey, skipDecodeBase64: true, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej(error.message || 'An error occurred'); }); }); } catch (error) { console.log(error); } }; export const handleUnencryptedPublishes = (publishes) => { let publishesData = []; publishes.forEach((pub) => { try { const decryptToUnit8Array = base64ToUint8Array(pub); const decodedData = uint8ArrayToObject(decryptToUnit8Array); if (decodedData) { publishesData.push({ decryptedData: decodedData }); } } catch (error) { console.log(error); } }); return publishesData; }; export const GroupAnnouncements = ({ selectedGroup, secretKey, setSecretKey, getSecretKey, myAddress, handleNewEncryptionNotification, isAdmin, hide, myName, isPrivate, }) => { 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 } = useContext(MyContext); const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = 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] = useReducer((x) => x + 1, 0); const { t } = useTranslation(['auth', 'core', 'group']); 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 }, isPrivate ) => { 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 = isPrivate === false ? handleUnencryptedPublishes([data]) : await decryptPublishes([{ data }], secretKey); const messageData = response[0]; if (!messageData) return; setAnnouncementData((prev) => { return { ...prev, [`${identifier}-${name}`]: messageData, }; }); } catch (error) { console.error('error', error); } }; useEffect(() => { if ( (!secretKey && isPrivate) || hasInitializedWebsocket.current || isPrivate === null ) return; setIsLoading(true); hasInitializedWebsocket.current = true; }, [secretKey, isPrivate]); const encryptChatMessage = async (data: string, secretKeyObject: any) => { try { return new Promise((res, rej) => { window .sendMessage('encryptSingle', { data, secretKeyObject, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirstChar', }) ); }); }); } catch (error) { console.log(error); } }; const publishAnc = async ({ encryptedData, identifier }: any) => { return new Promise((res, rej) => { window .sendMessage('publishGroupEncryptedResource', { encryptedData, identifier, }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirstChar', }) ); }); }); }; const clearEditorContent = () => { if (editorRef.current) { editorRef.current.chain().focus().clearContent().run(); } }; const setTempData = async (selectedGroup) => { try { const getTempAnnouncements = await getTempPublish(); if (getTempAnnouncements?.announcement) { let tempData = []; Object.keys(getTempAnnouncements?.announcement || {}) .filter((annKey) => annKey?.startsWith(`grp-${selectedGroup}-anc`)) .map((key) => { const value = getTempAnnouncements?.announcement[key]; tempData.push(value.data); }); setTempPublishedList(tempData); } } catch (error) { console.log(error); } }; const publishAnnouncement = async () => { try { pauseAllQueues(); const fee = await getFee('ARBITRARY'); await show({ message: t('core:message.question.perform_transaction', { action: 'ARBITRARY', postProcess: 'capitalizeFirstChar', }), 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 = isPrivate === false ? null : await getSecretKey(false, true); const message64: any = await objectToBase64(message); const encryptSingle = isPrivate === false ? message64 : 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(selectedGroup); clearEditorContent(); } // send chat message } catch (error) { if (!error) return; setInfoSnack({ type: 'error', message: error, }); setOpenSnack(true); } finally { resumeAllQueues(); setIsSending(false); } }; const getAnnouncements = useCallback( async (selectedGroup, isPrivate) => { 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(selectedGroup); setAnnouncements(responseData); setIsLoading(false); for (const data of responseData) { getAnnouncementData( { name: data.name, identifier: data.identifier, resource: data, }, isPrivate ); } } catch (error) { console.log(error); } }, [secretKey] ); useEffect(() => { if (!secretKey && isPrivate) return; if ( selectedGroup && !hasInitialized.current && !hide && isPrivate !== null ) { getAnnouncements(selectedGroup, isPrivate); hasInitialized.current = true; } }, [selectedGroup, secretKey, hide, isPrivate]); 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 }, isPrivate ); } } catch (error) { console.log(error); } }; const interval = useRef