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 && (
+
+ )}
+
+
+ );
+};