import React, { useCallback, useContext, useEffect, useRef, useState, } from 'react'; import { Avatar, Box, Button, ButtonBase, Collapse, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, MenuItem, Popover, Select, TextField, Typography, useTheme, } from '@mui/material'; import { LoadingButton } from '@mui/lab'; import LockIcon from '@mui/icons-material/Lock'; import NoEncryptionGmailerrorredIcon from '@mui/icons-material/NoEncryptionGmailerrorred'; import { MyContext, getArbitraryEndpointReact, getBaseApiReact, } from '../../App'; import { Spacer } from '../../common/Spacer'; import { CustomLoader } from '../../common/CustomLoader'; import { RequestQueueWithPromise } from '../../utils/queue/queue'; import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom, txListAtom, } from '../../atoms/global'; import { Label } from './AddGroup'; import ShortUniqueId from 'short-unique-id'; import { CustomizedSnackbars } from '../Snackbar/Snackbar'; import { getGroupNames } from './UserListOfInvites'; import { useVirtualizer } from '@tanstack/react-virtual'; import ErrorBoundary from '../../common/ErrorBoundary'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandLessIcon from '@mui/icons-material/ExpandLess'; import { getFee } from '../../background'; import { useAtom, useSetAtom } from 'jotai'; import { useTranslation } from 'react-i18next'; const THIRTY_MINUTES = 30 * 60 * 1000; // 30 minutes in milliseconds const uid = new ShortUniqueId({ length: 8 }); export const requestQueuePromos = new RequestQueueWithPromise(3); export function utf8ToBase64(inputString: string): string { // Encode the string as UTF-8 const utf8String = encodeURIComponent(inputString).replace( /%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(Number('0x' + p1)) ); // Convert the UTF-8 encoded string to base64 const base64String = btoa(utf8String); return base64String; } export function getGroupId(str) { const match = str.match(/group-(\d+)-/); return match ? match[1] : null; } export const ListOfGroupPromotions = () => { const [popoverAnchor, setPopoverAnchor] = useState(null); const [openPopoverIndex, setOpenPopoverIndex] = useState(null); const [selectedGroup, setSelectedGroup] = useState(null); const [loading, setLoading] = useState(false); const [isShowModal, setIsShowModal] = useState(false); const [text, setText] = useState(''); const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useAtom( myGroupsWhereIAmAdminAtom ); const [promotions, setPromotions] = useAtom(promotionsAtom); const [promotionTimeInterval, setPromotionTimeInterval] = useAtom( promotionTimeIntervalAtom ); const [isExpanded, setIsExpanded] = React.useState(false); const [openSnack, setOpenSnack] = useState(false); const [infoSnack, setInfoSnack] = useState(null); const [fee, setFee] = useState(null); const [isLoadingJoinGroup, setIsLoadingJoinGroup] = useState(false); const [isLoadingPublish, setIsLoadingPublish] = useState(false); const { show } = useContext(MyContext); const setTxList = useSetAtom(txListAtom); const theme = useTheme(); const { t } = useTranslation(['auth', 'core', 'group']); const listRef = useRef(null); const rowVirtualizer = useVirtualizer({ count: promotions.length, getItemKey: React.useCallback( (index) => promotions[index]?.identifier, [promotions] ), getScrollElement: () => listRef.current, estimateSize: () => 80, // Provide an estimated height of items, adjust this as needed overscan: 10, // Number of items to render outside the visible area to improve smoothness }); useEffect(() => { try { (async () => { const feeRes = await getFee('ARBITRARY'); setFee(feeRes?.fee); })(); } catch (error) { console.log(error); } }, []); const getPromotions = useCallback(async () => { try { setPromotionTimeInterval(Date.now()); const identifier = `group-promotions-ui24-`; const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT&identifier=${identifier}&limit=100&includemetadata=false&reverse=true&prefix=true`; const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); const responseData = await response.json(); let data: any[] = []; const uniqueGroupIds = new Set(); const oneWeekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; const getPromos = responseData?.map(async (promo: any) => { if (promo?.size < 200 && promo.created > oneWeekAgo) { const name = await requestQueuePromos.enqueue(async () => { const url = `${getBaseApiReact()}/arbitrary/${promo.service}/${ promo.name }/${promo.identifier}`; const response = await fetch(url, { method: 'GET', }); try { const responseData = await response.text(); if (responseData) { const groupId = getGroupId(promo.identifier); // Check if this groupId has already been processed if (!uniqueGroupIds.has(groupId)) { // Add the groupId to the set uniqueGroupIds.add(groupId); // Push the item to data data.push({ data: responseData, groupId, ...promo, }); } } } catch (error) { console.error('Error fetching promo:', error); } }); } return true; }); await Promise.all(getPromos); const groupWithInfo = await getGroupNames( data.sort((a, b) => b.created - a.created) ); setPromotions(groupWithInfo); } catch (error) { console.error(error); } }, []); useEffect(() => { const now = Date.now(); const timeSinceLastFetch = now - promotionTimeInterval; const initialDelay = timeSinceLastFetch >= THIRTY_MINUTES ? 0 : THIRTY_MINUTES - timeSinceLastFetch; const initialTimeout = setTimeout(() => { getPromotions(); // Start a 30-minute interval const interval = setInterval(() => { getPromotions(); }, THIRTY_MINUTES); return () => clearInterval(interval); }, initialDelay); return () => clearTimeout(initialTimeout); }, [getPromotions, promotionTimeInterval]); const handlePopoverOpen = (event, index) => { setPopoverAnchor(event.currentTarget); setOpenPopoverIndex(index); }; const handlePopoverClose = () => { setPopoverAnchor(null); setOpenPopoverIndex(null); }; const publishPromo = async () => { try { setIsLoadingPublish(true); const data = utf8ToBase64(text); const identifier = `group-promotions-ui24-group-${selectedGroup}-${uid.rnd()}`; await new Promise((res, rej) => { window .sendMessage('publishOnQDN', { data: data, identifier: identifier, service: 'DOCUMENT', }) .then((response) => { if (!response?.error) { res(response); return; } rej(response.error); }) .catch((error) => { rej( error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirst', }) ); }); }); setInfoSnack({ type: 'success', message: t('group:message.success.group_promotion', { postProcess: 'capitalizeFirst', }), }); setOpenSnack(true); setText(''); setSelectedGroup(null); setIsShowModal(false); } catch (error) { setInfoSnack({ type: 'error', message: error?.message || t('group:message.error.group_promotion', { postProcess: 'capitalizeFirst', }), }); setOpenSnack(true); } finally { setIsLoadingPublish(false); } }; const handleJoinGroup = async (group, isOpen) => { try { const groupId = group.groupId; const fee = await getFee('JOIN_GROUP'); await show({ message: t('core:message.question.perform_transaction', { action: 'JOIN_GROUP', postProcess: 'capitalizeFirst', }), publishFee: fee.fee + ' QORT', }); setIsLoadingJoinGroup(true); await new Promise((res, rej) => { window .sendMessage('joinGroup', { groupId, }) .then((response) => { if (!response?.error) { setInfoSnack({ type: 'success', message: t('group:message.success.group_join', { postProcess: 'capitalizeFirst', }), }); if (isOpen) { setTxList((prev) => [ { ...response, type: 'joined-group', label: t('group:message.success.group_join_label', { group_name: group?.groupName, postProcess: 'capitalizeFirst', }), labelDone: t('group:message.success.group_join_label', { group_name: group?.groupName, postProcess: 'capitalizeFirst', }), done: false, groupId, }, ...prev, ]); } else { setTxList((prev) => [ { ...response, type: 'joined-group-request', label: t('group:message.success.group_join_request', { group_name: group?.groupName, postProcess: 'capitalizeFirst', }), labelDone: t('group:message.success.group_join_outcome', { group_name: group?.groupName, postProcess: 'capitalizeFirst', }), done: false, groupId, }, ...prev, ]); } setOpenSnack(true); handlePopoverClose(); res(response); return; } else { setInfoSnack({ type: 'error', message: response?.error, }); setOpenSnack(true); rej(response.error); } }) .catch((error) => { setInfoSnack({ type: 'error', message: error.message || t('core:message.error.generic', { postProcess: 'capitalizeFirst', }), }); setOpenSnack(true); rej(error); }); }); setIsLoadingJoinGroup(false); } catch (error) { console.log(error); } finally { setIsLoadingJoinGroup(false); } }; return ( setIsExpanded((prev) => !prev)} > {t('group:group.promotions', { postProcess: 'capitalizeFirst' })}{' '} {promotions.length > 0 && ` (${promotions.length})`} {isExpanded ? ( ) : ( )} <> {loading && promotions.length === 0 && ( )} {!loading && promotions.length === 0 && ( {t('group.message.generic.no_display', { postProcess: 'capitalizeFirst', })} )}
{rowVirtualizer.getVirtualItems().map((virtualRow) => { const index = virtualRow.index; const promotion = promotions[index]; return (
{t('group:message.generic.invalid_data', { postProcess: 'capitalizeFirst', })} } > { if (reason === 'backdropClick') { // Prevent closing on backdrop click return; } handlePopoverClose(); // Close only on other events like Esc key press }} anchorOrigin={{ vertical: 'top', horizontal: 'center', }} transformOrigin={{ vertical: 'bottom', horizontal: 'center', }} style={{ marginTop: '8px' }} > {t('group:group.name', { postProcess: 'capitalizeFirst', })} : {` ${promotion?.groupName}`} {t('group:group.member_number', { postProcess: 'capitalizeFirst', })} : {` ${promotion?.memberCount}`} {promotion?.description && ( {promotion?.description} )} {promotion?.isOpen === false && ( {t('group:message.generic.closed_group', { postProcess: 'capitalizeFirst', })} )} {t('core:action.close', { postProcess: 'capitalizeFirst', })} handleJoinGroup( promotion, promotion?.isOpen ) } > {t('core:action.join', { postProcess: 'capitalizeFirst', })} {promotion?.name?.charAt(0)} {promotion?.name} {promotion?.groupName} {promotion?.isOpen === false && ( )} {promotion?.isOpen === true && ( )} {promotion?.isOpen ? t('group:group.public', { postProcess: 'capitalizeFirst', }) : t('group:group.private', { postProcess: 'capitalizeFirst', })} {promotion?.data}
); })}
{t('group:action.promote_group', { postProcess: 'capitalizeFirst' })} {t('group:message.generic.latest_promotion', { postProcess: 'capitalizeFirst', })} {t('group:message.generic.max_chars', { postProcess: 'capitalizeFirst', })} : {fee && fee} {' QORT'} setText(e.target.value)} inputProps={{ maxLength: 200, }} multiline={true} sx={{ '& .MuiFormLabel-root': { color: theme.palette.text.primary, }, '& .MuiFormLabel-root.Mui-focused': { color: theme.palette.text.primary, }, }} />
); };