From c4b09db91d851266ceb1c216c030628d2ee12d33 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 14 Nov 2024 06:46:33 +0200 Subject: [PATCH] added group promotions --- src/atoms/global.ts | 15 + src/components/Group/GroupJoinRequests.tsx | 8 +- src/components/Group/Home.tsx | 4 + src/components/Group/HomeDesktop.tsx | 4 + .../Group/ListOfGroupPromotions.tsx | 774 ++++++++++++++++++ 5 files changed, 804 insertions(+), 1 deletion(-) create mode 100644 src/components/Group/ListOfGroupPromotions.tsx diff --git a/src/atoms/global.ts b/src/atoms/global.ts index 3688c0b..0245249 100644 --- a/src/atoms/global.ts +++ b/src/atoms/global.ts @@ -73,4 +73,19 @@ export const hasSettingsChangedAtom = atom({ export const navigationControllerAtom = atom({ key: 'navigationControllerAtom', default: {}, +}); + +export const myGroupsWhereIAmAdminAtom = atom({ + key: 'myGroupsWhereIAmAdminAtom', + default: [], +}); + +export const promotionTimeIntervalAtom = atom({ + key: 'promotionTimeIntervalAtom', + default: 0, +}); + +export const promotionsAtom = atom({ + key: 'promotionsAtom', + default: [], }); \ No newline at end of file diff --git a/src/components/Group/GroupJoinRequests.tsx b/src/components/Group/GroupJoinRequests.tsx index 47fcb31..fef5862 100644 --- a/src/components/Group/GroupJoinRequests.tsx +++ b/src/components/Group/GroupJoinRequests.tsx @@ -16,13 +16,17 @@ import { Spacer } from "../../common/Spacer"; import { CustomLoader } from "../../common/CustomLoader"; import { getBaseApi } from "../../background"; import { MyContext, getBaseApiReact, isMobile } from "../../App"; +import { myGroupsWhereIAmAdminAtom } from "../../atoms/global"; +import { useSetRecoilState } from "recoil"; export const requestQueueGroupJoinRequests = new RequestQueueWithPromise(2) export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, getTimestampEnterChat, setSelectedGroup, setGroupSection, setMobileViewMode }) => { const [groupsWithJoinRequests, setGroupsWithJoinRequests] = React.useState([]) const [loading, setLoading] = React.useState(true) const {txList, setTxList} = React.useContext(MyContext) - + const setMyGroupsWhereIAmAdmin = useSetRecoilState( + myGroupsWhereIAmAdminAtom + ); const getJoinRequests = async ()=> { @@ -50,6 +54,8 @@ export const GroupJoinRequests = ({ myAddress, groups, setOpenManageMembers, get await Promise.all(getAllGroupsAsAdmin) + setMyGroupsWhereIAmAdmin(groupsAsAdmin) + const res = await Promise.all(groupsAsAdmin.map(async (group)=> { const joinRequestResponse = await requestQueueGroupJoinRequests.enqueue(()=> { diff --git a/src/components/Group/Home.tsx b/src/components/Group/Home.tsx index 4d8e296..2109355 100644 --- a/src/components/Group/Home.tsx +++ b/src/components/Group/Home.tsx @@ -6,6 +6,7 @@ import { ThingsToDoInitial } from "./ThingsToDoInitial"; import { GroupJoinRequests } from "./GroupJoinRequests"; import { GroupInvites } from "./GroupInvites"; import RefreshIcon from "@mui/icons-material/Refresh"; +import { ListOfGroupPromotions } from "./ListOfGroupPromotions"; export const Home = ({ refreshHomeDataFunc, @@ -105,6 +106,9 @@ export const Home = ({ /> )} + {!isLoadingGroups && ( + + )} ); diff --git a/src/components/Group/HomeDesktop.tsx b/src/components/Group/HomeDesktop.tsx index 08d3190..59400f6 100644 --- a/src/components/Group/HomeDesktop.tsx +++ b/src/components/Group/HomeDesktop.tsx @@ -6,6 +6,7 @@ import { ThingsToDoInitial } from "./ThingsToDoInitial"; import { GroupJoinRequests } from "./GroupJoinRequests"; import { GroupInvites } from "./GroupInvites"; import RefreshIcon from "@mui/icons-material/Refresh"; +import { ListOfGroupPromotions } from "./ListOfGroupPromotions"; export const HomeDesktop = ({ refreshHomeDataFunc, @@ -122,6 +123,9 @@ export const HomeDesktop = ({ )} + {!isLoadingGroups && ( + + )} diff --git a/src/components/Group/ListOfGroupPromotions.tsx b/src/components/Group/ListOfGroupPromotions.tsx new file mode 100644 index 0000000..51f04b5 --- /dev/null +++ b/src/components/Group/ListOfGroupPromotions.tsx @@ -0,0 +1,774 @@ +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; +import { + Avatar, + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, + MenuItem, + Popover, + Select, + TextField, + Typography, +} from "@mui/material"; +import { + AutoSizer, + CellMeasurer, + CellMeasurerCache, + List, +} from "react-virtualized"; +import { getNameInfo } from "./Group"; +import { getBaseApi, getFee } from "../../background"; +import { LoadingButton } from "@mui/lab"; +import { + MyContext, + getArbitraryEndpointReact, + getBaseApiReact, + isMobile, +} from "../../App"; +import { Spacer } from "../../common/Spacer"; +import { CustomLoader } from "../../common/CustomLoader"; +import { RequestQueueWithPromise } from "../../utils/queue/queue"; +import { useRecoilState } from "recoil"; +import { myGroupsWhereIAmAdminAtom, promotionTimeIntervalAtom, promotionsAtom } from "../../atoms/global"; +import { Label } from "./AddGroup"; +import ShortUniqueId from "short-unique-id"; +import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { getGroupNames } from "./UserListOfInvites"; +import { WrapperUserAction } from "../WrapperUserAction"; + +export const requestQueuePromos = new RequestQueueWithPromise(20); + +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 }); + +const cache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 50, +}); + +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] = useRecoilState( + myGroupsWhereIAmAdminAtom + ); + const [promotions, setPromotions] = useRecoilState( + promotionsAtom + ); + const [promotionTimeInterval, setPromotionTimeInterval] = useRecoilState( + promotionTimeIntervalAtom + ); + 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, setTxList } = useContext(MyContext); + + const listRef = useRef(); + + useEffect(() => { + try { + (async () => { + const feeRes = await getFee("ARBITRARY"); + setFee(feeRes?.fee); + })(); + } catch (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); + 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]); + + 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"); + }); + }); + 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) => { + chrome?.runtime?.sendMessage( + { + action: "joinGroup", + payload: { + groupId, + }, + }, + (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); + } + } + ); + }); + + setIsLoadingJoinGroup(false); + } catch (error) { + } finally { + setIsLoadingJoinGroup(false); + } + }; + + // const handleCancelInvitation = async (address)=> { + // try { + // const fee = await getFee('CANCEL_GROUP_INVITE') + // await show({ + // message: "Would you like to perform a CANCEL_GROUP_INVITE transaction?" , + // publishFee: fee.fee + ' QORT' + // }) + // setIsLoadingCancelInvite(true) + // await new Promise((res, rej)=> { + // window.sendMessage("cancelInvitationToGroup", { + // groupId, + // qortalAddress: address, + // }) + // .then((response) => { + // if (!response?.error) { + // setInfoSnack({ + // type: "success", + // message: "Successfully canceled invitation. It may take a couple of minutes for the changes to propagate", + // }); + // setOpenSnack(true); + // handlePopoverClose(); + // setIsLoadingCancelInvite(true); + // res(response); + // return; + // } + // 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); + // }); + + // }) + // } catch (error) { + + // } finally { + // setIsLoadingCancelInvite(false) + // } + // } + + const rowRenderer = ({ index, key, parent, style }) => { + const promotion = promotions[index]; + + return ( + + {({ measure }) => ( +
+ + { + 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?.data} + + + + + + + +
+ )} +
+ ); + }; + + + return ( + + + + + Group Promotions + + + + + + + + {loading && promotions.length === 0 && ( + + + + )} + {!loading && promotions.length === 0 && ( + + + Nothing to display + + + )} +
+ + {({ height, width }) => ( + + )} + +
+
+ + + {isShowModal && ( + + + {"Publish Group Promotion"} + + + + 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: "white", + }, + "& .MuiFormLabel-root.Mui-focused": { + color: "white", + }, + }} + /> + + + + + + + )} + +
+ ); +};