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'; 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; } const uid = new ShortUniqueId({ length: 8 }); export function getGroupId(str) { const match = str.match(/group-(\d+)-/); return match ? match[1] : null; } const THIRTY_MINUTES = 30 * 60 * 1000; // 30 minutes in milliseconds 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 listRef = useRef(); 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 || 'An error occurred'); }); }); // TODO translate setInfoSnack({ type: 'success', message: 'Successfully published promotion. It may take a couple of minutes for the promotion to appear', }); setOpenSnack(true); setText(''); setSelectedGroup(null); setIsShowModal(false); } catch (error) { setInfoSnack({ type: 'error', message: error?.message || 'Error publishing the promotion. Please try again', }); setOpenSnack(true); } finally { setIsLoadingPublish(false); } }; const handleJoinGroup = async (group, isOpen) => { try { const groupId = group.groupId; const fee = await getFee('JOIN_GROUP'); await show({ message: 'Would you like to perform an JOIN_GROUP transaction?', publishFee: fee.fee + ' QORT', }); setIsLoadingJoinGroup(true); await new Promise((res, rej) => { window .sendMessage('joinGroup', { groupId, }) .then((response) => { if (!response?.error) { setInfoSnack({ type: 'success', message: 'Successfully requested to join group. It may take a couple of minutes for the changes to propagate', }); if (isOpen) { setTxList((prev) => [ { ...response, type: 'joined-group', label: `Joined Group ${group?.groupName}: awaiting confirmation`, labelDone: `Joined Group ${group?.groupName}: success!`, done: false, groupId, }, ...prev, ]); } else { setTxList((prev) => [ { ...response, type: 'joined-group-request', label: `Requested to join Group ${group?.groupName}: awaiting confirmation`, labelDone: `Requested to join Group ${group?.groupName}: success!`, 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 || 'An error occurred', }); setOpenSnack(true); rej(error); }); }); setIsLoadingJoinGroup(false); } catch (error) { } finally { setIsLoadingJoinGroup(false); } }; return ( setIsExpanded((prev) => !prev)} > Group promotions{' '} {promotions.length > 0 && ` (${promotions.length})`} {isExpanded ? ( ) : ( )} <> {loading && promotions.length === 0 && ( )} {!loading && promotions.length === 0 && ( Nothing to display )}
{rowVirtualizer.getVirtualItems().map((virtualRow) => { const index = virtualRow.index; const promotion = promotions[index]; return (
Error loading content: Invalid Data } > { 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' }} > Group name: {` ${promotion?.groupName}`} Number of members:{' '} {` ${promotion?.memberCount}`} {promotion?.description && ( {promotion?.description} )} {promotion?.isOpen === false && ( *This is a closed/private group, so you will need to wait until an admin accepts your request )} Close handleJoinGroup( promotion, promotion?.isOpen ) } > Join {promotion?.name?.charAt(0)} {promotion?.name} {promotion?.groupName} {promotion?.isOpen === false && ( )} {promotion?.isOpen === true && ( )} {promotion?.isOpen ? 'Public group' : 'Private group'} {promotion?.data}
); })}
{isShowModal && ( {'Promote your group to non-members'} Only the latest promotion from the week will be shown for your group. Max 200 characters. Publish Fee: {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, }, }} /> )}
); };