import React, { FC, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Avatar, Box, Popover, Typography } from "@mui/material"; // import { MAIL_SERVICE_TYPE, THREAD_SERVICE_TYPE } from "../../constants/mail"; import { Thread } from "./Thread"; import { AllThreadP, ArrowDownIcon, ComposeContainer, ComposeContainerBlank, ComposeIcon, ComposeP, GroupContainer, GroupNameP, InstanceFooter, InstanceListContainer, InstanceListContainerRow, InstanceListContainerRowCheck, InstanceListContainerRowCheckIcon, InstanceListContainerRowMain, InstanceListContainerRowMainP, InstanceListHeader, InstanceListParent, SelectInstanceContainerFilterInner, SingleThreadParent, ThreadContainer, ThreadContainerFullWidth, ThreadInfoColumn, ThreadInfoColumnNameP, ThreadInfoColumnTime, ThreadInfoColumnbyP, ThreadSingleLastMessageP, ThreadSingleLastMessageSpanP, ThreadSingleTitle, } from "./Mail-styles"; import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; import { Spacer } from "../../../common/Spacer"; import { formatDate, formatTimestamp } from "../../../utils/time"; import LazyLoad from "../../../common/LazyLoad"; import { delay } from "../../../utils/helpers"; import { NewThread } from "./NewThread"; import { getBaseApi } from "../../../background"; import { decryptPublishes, getTempPublish } from "../../Chat/GroupAnnouncements"; import CheckSVG from "../../../assets/svgs/Check.svg"; import SortSVG from "../../../assets/svgs/Sort.svg"; import ArrowDownSVG from "../../../assets/svgs/ArrowDown.svg"; import { LoadingSnackbar } from "../../Snackbar/LoadingSnackbar"; import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../../utils/events"; import RefreshIcon from '@mui/icons-material/Refresh'; import { getArbitraryEndpointReact, getBaseApiReact } from "../../../App"; import { WrapperUserAction } from "../../WrapperUserAction"; import { addDataPublishesFunc, getDataPublishesFunc } from "../Group"; const filterOptions = ["Recently active", "Newest", "Oldest"]; export const threadIdentifier = "DOCUMENT"; export const GroupMail = ({ selectedGroup, userInfo, getSecretKey, secretKey, defaultThread, setDefaultThread, hide }) => { const [viewedThreads, setViewedThreads] = React.useState({}); const [filterMode, setFilterMode] = useState("Recently active"); const [currentThread, setCurrentThread] = React.useState(null); const [recentThreads, setRecentThreads] = useState([]); const [allThreads, setAllThreads] = useState([]); const [members, setMembers] = useState(null); const [isOpenFilterList, setIsOpenFilterList] = useState(false); const anchorElInstanceFilter = useRef(null); const [tempPublishedList, setTempPublishedList] = useState([]) const dataPublishes = useRef({}) const [isLoading, setIsLoading] = useState(false) const groupIdRef = useRef(null); const groupId = useMemo(() => { return selectedGroup?.groupId; }, [selectedGroup]); useEffect(()=> { if(!groupId) return (async ()=> { const res = await getDataPublishesFunc(groupId, 'thread') dataPublishes.current = res || {} })() }, [groupId]) useEffect(() => { if (groupId !== groupIdRef?.current) { setCurrentThread(null); setRecentThreads([]); setAllThreads([]); groupIdRef.current = groupId; } }, [groupId]); const setTempData = async ()=> { try { const getTempAnnouncements = await getTempPublish() if(getTempAnnouncements?.thread){ let tempData = [] Object.keys(getTempAnnouncements?.thread || {}).map((key)=> { const value = getTempAnnouncements?.thread[key] tempData.push(value.data) }) setTempPublishedList(tempData) } } catch (error) { } } const getEncryptedResource = async ({ name, identifier, resource }) => { let data = dataPublishes.current[`${name}-${identifier}`] if(!data || (data?.update || data?.created !== (resource?.updated || resource?.created))){ const res = await fetch( `${getBaseApiReact()}/arbitrary/DOCUMENT/${name}/${identifier}?encoding=base64` ); if(!res?.ok) return data = await res.text(); await addDataPublishesFunc({...resource, data}, groupId, 'thread') } else { data = data.data } const response = await decryptPublishes([{ data }], secretKey); const messageData = response[0]; return messageData.decryptedData; }; const updateThreadActivity = async ({threadId, qortalName, groupId, thread}) => { try { await new Promise((res, rej) => { chrome?.runtime?.sendMessage( { action: "updateThreadActivity", payload: { threadId, qortalName, groupId, thread }, }, (response) => { if (!response?.error) { res(response); return } rej(response.error); } ); }); } catch (error) { } finally { } }; const getAllThreads = React.useCallback( async (groupId: string, mode: string, isInitial?: boolean) => { try { setIsLoading(true) const offset = isInitial ? 0 : allThreads.length; const isReverse = mode === "Newest" ? true : false; if (isInitial) { // dispatch(setIsLoadingCustom("Loading threads")); } const identifier = `grp-${groupId}-thread-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=${20}&includemetadata=false&offset=${offset}&reverse=${isReverse}&prefix=true`; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json", }, }); const responseData = await response.json(); let fullArrayMsg = isInitial ? [] : [...allThreads]; const getMessageForThreads = responseData.map(async (message: any) => { let fullObject: any = null; if (message?.metadata?.description) { fullObject = { ...message, threadData: { title: message?.metadata?.description, groupId: groupId, createdAt: message?.created, name: message?.name, }, threadOwner: message?.name, }; } else { let threadRes = null; try { threadRes = await Promise.race([ getEncryptedResource({ name: message.name, identifier: message.identifier, resource: message }), delay(5000), ]); } catch (error) {} if (threadRes?.title) { fullObject = { ...message, threadData: threadRes, threadOwner: message?.name, threadId: message.identifier }; } } if (fullObject?.identifier) { const index = fullArrayMsg.findIndex( (p) => p.identifier === fullObject.identifier ); if (index !== -1) { fullArrayMsg[index] = fullObject; } else { fullArrayMsg.push(fullObject); } } }); await Promise.all(getMessageForThreads); let sorted = fullArrayMsg; if (isReverse) { sorted = fullArrayMsg.sort((a: any, b: any) => b.created - a.created); } else { sorted = fullArrayMsg.sort((a: any, b: any) => a.created - b.created); } setAllThreads(sorted); } catch (error) { console.log({ error }); } finally { if (isInitial) { setIsLoading(false) // dispatch(setIsLoadingCustom(null)); } } }, [allThreads] ); const getMailMessages = React.useCallback( async (groupId: string, members: any) => { try { setIsLoading(true) // const memberNames = Object.keys(members); // const queryString = memberNames // .map(name => `&name=${encodeURIComponent(name)}`) // .join(""); // dispatch(setIsLoadingCustom("Loading recent threads")); const identifier = `thmsg-grp-${groupId}-thread-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifier}&limit=100&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 messagesForThread: any = {}; for (const message of responseData) { let str = message.identifier; const parts = str.split("-"); // Get the second last element const secondLastId = parts[parts.length - 2]; const result = `grp-${groupId}-thread-${secondLastId}`; const checkMessage = messagesForThread[result]; if (!checkMessage) { messagesForThread[result] = message; } } const newArray = Object.keys(messagesForThread) .map((key) => { return { ...messagesForThread[key], threadId: key, }; }) .sort((a, b) => b.created - a.created) .slice(0, 10); let fullThreadArray: any = []; const getMessageForThreads = newArray.map(async (message: any) => { try { const identifierQuery = message.threadId; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=${threadIdentifier}&identifier=${identifierQuery}&limit=1&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(); if (responseData.length > 0) { const thread = responseData[0]; if (thread?.metadata?.description) { const fullObject = { ...message, threadData: { title: thread?.metadata?.description, groupId: groupId, createdAt: thread?.created, name: thread?.name, }, threadOwner: thread?.name, }; fullThreadArray.push(fullObject); } else { let threadRes = await Promise.race([ getEncryptedResource({ name: thread.name, identifier: message.threadId, resource: thread }), delay(10000), ]); if (threadRes?.title) { const fullObject = { ...message, threadData: threadRes, threadOwner: thread?.name, }; fullThreadArray.push(fullObject); } } } } catch (error) { console.log(error); } return null; }); await Promise.all(getMessageForThreads); const sorted = fullThreadArray.sort( (a: any, b: any) => b.created - a.created ); setRecentThreads(sorted); } catch (error) { } finally { setIsLoading(false) // dispatch(setIsLoadingCustom(null)); } }, [secretKey] ); const getMessages = React.useCallback(async () => { // if ( !groupId || members?.length === 0) return; if (!groupId) return; await getMailMessages(groupId, members); }, [getMailMessages, groupId, members, secretKey]); const interval = useRef(null); const firstMount = useRef(false); const filterModeRef = useRef(""); useEffect(() => { if(hide) return if (filterModeRef.current !== filterMode) { firstMount.current = false; } // if (groupId && !firstMount.current && members.length > 0) { if (groupId && !firstMount.current) { if (filterMode === "Recently active") { getMessages(); } else if (filterMode === "Newest") { getAllThreads(groupId, "Newest", true); } else if (filterMode === "Oldest") { getAllThreads(groupId, "Oldest", true); } setTempData() firstMount.current = true; } }, [groupId, members, filterMode, hide]); const closeThread = useCallback(() => { setCurrentThread(null); }, []); const getGroupMembers = useCallback(async (groupNumber: string) => { try { const response = await fetch(`/groups/members/${groupNumber}?limit=0`); const groupData = await response.json(); let members: any = {}; if (groupData && Array.isArray(groupData?.members)) { for (const member of groupData.members) { if (member.member) { // const res = await getNameInfo(member.member); // const resAddress = await qortalRequest({ // action: "GET_ACCOUNT_DATA", // address: member.member, // }); const name = res; const publicKey = resAddress.publicKey; if (name) { members[name] = { publicKey, address: member.member, }; } } } } setMembers(members); } catch (error) { console.log({ error }); } }, []); // useEffect(() => { // if(groupId){ // getGroupMembers(groupId); // interval.current = setInterval(async () => { // getGroupMembers(groupId); // }, 180000) // } // return () => { // if (interval?.current) { // clearInterval(interval.current) // } // } // }, [getGroupMembers, groupId]); let listOfThreadsToDisplay = recentThreads; if (filterMode === "Newest" || filterMode === "Oldest") { listOfThreadsToDisplay = allThreads; } const onSubmitNewThread = useCallback( (val: any) => { if (filterMode === "Recently active") { setRecentThreads((prev) => [val, ...prev]); } else if (filterMode === "Newest") { setAllThreads((prev) => [val, ...prev]); } }, [filterMode] ); // useEffect(()=> { // if(user?.name){ // const threads = JSON.parse( // localStorage.getItem(`qmail_threads_viewedtimestamp_${user.name}`) || "{}" // ); // setViewedThreads(threads) // } // }, [user?.name, currentThread]) const handleCloseThreadFilterList = () => { setIsOpenFilterList(false); }; const refetchThreadsLists = useCallback(()=> { if (filterMode === "Recently active") { getMessages(); } else if (filterMode === "Newest") { getAllThreads(groupId, "Newest", true); } else if (filterMode === "Oldest") { getAllThreads(groupId, "Oldest", true); } }, [filterMode]) const updateThreadActivityCurrentThread = ()=> { if(!currentThread) return const thread = currentThread updateThreadActivity({ threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread }) } const setThreadFunc = (data)=> { const thread = data setCurrentThread(thread); if(thread?.threadId && thread?.threadData?.name){ updateThreadActivity({ threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread }) } setTimeout(() => { executeEvent("threadFetchMode", { mode: "last-page" }); }, 300); } useEffect(()=> { if(defaultThread){ setThreadFunc(defaultThread) setDefaultThread(null) } }, [defaultThread]) const combinedListTempAndReal = useMemo(() => { // Combine the two lists const transformTempPublishedList = tempPublishedList.map((item)=> { return { ...item, threadData: item.tempData, threadOwner: item?.name, threadId: item.identifier } }) const combined = [...transformTempPublishedList, ...listOfThreadsToDisplay]; // Remove duplicates based on the "identifier" const uniqueItems = new Map(); combined.forEach(item => { uniqueItems.set(item.threadId, 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.threadData?.createdAt - a.threadData?.createdAt); return sortedList; }, [tempPublishedList, listOfThreadsToDisplay]); if (currentThread) return ( ); return ( {filterOptions?.map((filter) => { return ( { setFilterMode(filter); }} sx={{ backgroundColor: filterMode === filter ? "rgba(74, 158, 244, 1)" : "unset", }} key={filter} > {filter === filterMode && ( )} {filter} ); })} {selectedGroup && !currentThread && ( { setIsOpenFilterList(true); }} ref={anchorElInstanceFilter} > Sort by )} {filterMode} {combinedListTempAndReal.map((thread) => { const hasViewedRecent = viewedThreads[ `qmail_threads_${thread?.threadData?.groupId}_${thread?.threadId}` ]; const shouldAppearLighter = hasViewedRecent && filterMode === "Recently active" && thread?.threadData?.createdAt < hasViewedRecent?.timestamp; return ( { setCurrentThread(thread); if(thread?.threadId && thread?.threadData?.name){ updateThreadActivity({ threadId: thread?.threadId, qortalName: thread?.threadData?.name, groupId: groupId, thread: thread }) } }} > {thread?.threadData?.name?.charAt(0)} by {thread?.threadData?.name} {formatTimestamp(thread?.threadData?.createdAt)}
{thread?.threadData?.title} {filterMode === "Recently active" && (
last message:{" "} {formatDate(thread?.created)}
)}
{ setTimeout(() => { executeEvent("threadFetchMode", { mode: "last-page" }); }, 300); }} sx={{ position: 'absolute', bottom: '2px', right: '2px', borderRadius: '5px', backgroundColor: '#27282c', display: 'flex', gap: '10px', alignItems: 'center', padding: '5px', cursor: 'pointer', '&:hover': { background: 'rgba(255, 255, 255, 0.60)' } }}> Last page
); })} {listOfThreadsToDisplay.length >= 20 && filterMode !== "Recently active" && ( getAllThreads(groupId, filterMode, false)} > )}
); };