diff --git a/src/App.tsx b/src/App.tsx index 310dc21..eb80967 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -124,6 +124,7 @@ import { useHandleUserInfo } from "./components/Group/useHandleUserInfo"; import { Minting } from "./components/Minting/Minting"; import { isRunningGateway } from "./qortalRequests"; import { QMailStatus } from "./components/QMailStatus"; +import { GlobalActions } from "./components/GlobalActions/GlobalActions"; type extStates = | "not-authenticated" @@ -1686,6 +1687,7 @@ function App() { }} > + )} diff --git a/src/components/Chat/MessageDisplay.tsx b/src/components/Chat/MessageDisplay.tsx index 394a0bf..b78cbc9 100644 --- a/src/components/Chat/MessageDisplay.tsx +++ b/src/components/Chat/MessageDisplay.tsx @@ -97,6 +97,28 @@ export const MessageDisplay = ({ htmlContent, isReply }) => { window.electronAPI.openExternal(href); } else if (target.getAttribute('data-url')) { const url = target.getAttribute('data-url'); + + let copyUrl = url + + try { + copyUrl = copyUrl.replace(/^(qortal:\/\/)/, '') + if (copyUrl.startsWith('use-')) { + // Handle the new 'use' format + const parts = copyUrl.split('/') + const type = parts[0].split('-')[1] // e.g., 'group' from 'use-group' + parts.shift() + const action = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., 'invite' from 'action-invite' + parts.shift() + const idPrefix = parts.length > 0 ? parts[0].split('-')[0] : null // e.g., 'groupid' from 'groupid-321' + const id = parts.length > 0 ? parts[0].split('-')[1] : null // e.g., '321' from 'groupid-321' + if(action === 'join'){ + executeEvent("globalActionJoinGroup", { groupId: id}); + return + } + } + } catch (error) { + //error + } const res = extractComponents(url); if (res) { const { service, name, identifier, path } = res; diff --git a/src/components/GlobalActions/GlobalActions.tsx b/src/components/GlobalActions/GlobalActions.tsx new file mode 100644 index 0000000..6dd845c --- /dev/null +++ b/src/components/GlobalActions/GlobalActions.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { JoinGroup } from './JoinGroup' + +export const GlobalActions = ({memberGroups}) => { + return ( + <> + + + ) +} diff --git a/src/components/GlobalActions/JoinGroup.tsx b/src/components/GlobalActions/JoinGroup.tsx new file mode 100644 index 0000000..3eedd6c --- /dev/null +++ b/src/components/GlobalActions/JoinGroup.tsx @@ -0,0 +1,272 @@ +import React, { useContext, useEffect, useMemo, useState } from "react"; +import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; +import { + Box, + Button, + ButtonBase, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + Typography, +} from "@mui/material"; +import { CustomButton, CustomButtonAccept } from "../../App-styles"; +import { getBaseApiReact, MyContext } from "../../App"; +import { getFee } from "../../background"; +import { CustomizedSnackbars } from "../Snackbar/Snackbar"; +import { FidgetSpinner } from "react-loader-spinner"; + +export const JoinGroup = ({ memberGroups }) => { + const { show, setTxList } = useContext(MyContext); + const [openSnack, setOpenSnack] = useState(false); + const [infoSnack, setInfoSnack] = useState(null); + const [groupInfo, setGroupInfo] = useState(null); + const [isLoadingInfo, setIsLoadingInfo] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [isLoadingJoinGroup, setIsLoadingJoinGroup] = useState(false); + const handleJoinGroup = async (e) => { + setGroupInfo(null); + const groupId = e?.detail?.groupId; + if (groupId) { + try { + setIsOpen(true); + setIsLoadingInfo(true); + const response = await fetch(`${getBaseApiReact()}/groups/${groupId}`); + const groupData = await response.json(); + setGroupInfo(groupData); + } catch (error) { + } finally { + setIsLoadingInfo(false); + } + } + }; + + useEffect(() => { + subscribeToEvent("globalActionJoinGroup", handleJoinGroup); + + return () => { + unsubscribeFromEvent("globalActionJoinGroup", handleJoinGroup); + }; + }, []); + + const isInGroup = useMemo(()=> { + return !!memberGroups.find((item)=> +item?.groupId === +groupInfo?.groupId) + }, [memberGroups, groupInfo]) + const joinGroup = 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); + res(response); + return; + } else { + setInfoSnack({ + type: "error", + message: response?.error, + }); + setOpenSnack(true); + rej(response.error); + } + } + ); + }); + setIsLoadingJoinGroup(false); + } catch (error) { + } finally { + setIsLoadingJoinGroup(false); + } + }; + return ( + <> + + + {!groupInfo && ( + + {" "} + {" "} + + )} + + + Group name: {` ${groupInfo?.groupName}`} + + + Number of members: {` ${groupInfo?.memberCount}`} + + {groupInfo?.description && ( + + {groupInfo?.description} + + )} + {isInGroup && ( + + *You are already in this group! + + )} + {!isInGroup && groupInfo?.isOpen === false && ( + + *This is a closed/private group, so you will need to wait until + an admin accepts your request + + )} + + + + { + joinGroup(groupInfo, groupInfo?.isOpen); + + setIsOpen(false); + }} disabled={isInGroup}> + + Join + + + + setIsOpen(false)} + > + Close + + + + + + {isLoadingJoinGroup && ( + + + + )} + + ); +}; diff --git a/src/components/Tutorials/img/creation.webp b/src/components/Tutorials/img/creation.webp new file mode 100644 index 0000000..46310e0 Binary files /dev/null and b/src/components/Tutorials/img/creation.webp differ diff --git a/src/components/Tutorials/img/dashboard.webp b/src/components/Tutorials/img/dashboard.webp new file mode 100644 index 0000000..6e33ffd Binary files /dev/null and b/src/components/Tutorials/img/dashboard.webp differ diff --git a/src/components/Tutorials/img/groups.webp b/src/components/Tutorials/img/groups.webp new file mode 100644 index 0000000..8a0ad22 Binary files /dev/null and b/src/components/Tutorials/img/groups.webp differ diff --git a/src/components/Tutorials/img/important.webp b/src/components/Tutorials/img/important.webp new file mode 100644 index 0000000..880a84e Binary files /dev/null and b/src/components/Tutorials/img/important.webp differ diff --git a/src/components/Tutorials/img/navigation.webp b/src/components/Tutorials/img/navigation.webp new file mode 100644 index 0000000..a568960 Binary files /dev/null and b/src/components/Tutorials/img/navigation.webp differ diff --git a/src/components/Tutorials/img/overview.webp b/src/components/Tutorials/img/overview.webp new file mode 100644 index 0000000..bc413b4 Binary files /dev/null and b/src/components/Tutorials/img/overview.webp differ diff --git a/src/components/Tutorials/img/started.webp b/src/components/Tutorials/img/started.webp new file mode 100644 index 0000000..76099a5 Binary files /dev/null and b/src/components/Tutorials/img/started.webp differ diff --git a/src/components/Tutorials/useHandleTutorials.tsx b/src/components/Tutorials/useHandleTutorials.tsx index 468dc4c..decf6f0 100644 --- a/src/components/Tutorials/useHandleTutorials.tsx +++ b/src/components/Tutorials/useHandleTutorials.tsx @@ -1,6 +1,12 @@ import React, { useCallback, useEffect, useState } from "react"; import { saveToLocalStorage } from "../Apps/AppsNavBar"; - +import creationImg from './img/creation.webp' +import dashboardImg from './img/dashboard.webp' +import groupsImg from './img/groups.webp' +import importantImg from './img/important.webp' +import navigationImg from './img/navigation.webp' +import overviewImg from './img/overview.webp' +import startedImg from './img/started.webp' const checkIfGatewayIsOnline = async () => { try { @@ -61,102 +67,108 @@ useEffect(()=> { const isOnline = await checkIfGatewayIsOnline() if(!isOnline) return switch (type) { - case "create-account": - { - if((shownTutorials || {})['create-account'] && !isForce) return - saveShowTutorial('create-account') - setOpenTutorialModal({ - title: "Account Creation", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "account-creation-hub", - }, - }); - } - break; - case "important-information": - { - if((shownTutorials || {})['important-information'] && !isForce) return - saveShowTutorial('important-information') + case "create-account": + { + if((shownTutorials || {})['create-account'] && !isForce) return + saveShowTutorial('create-account') + setOpenTutorialModal({ + title: "Account Creation", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "account-creation-hub", + poster: creationImg + }, + }); + } + break; + case "important-information": + { + if((shownTutorials || {})['important-information'] && !isForce) return + saveShowTutorial('important-information') - setOpenTutorialModal({ - title: "Important Information!", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "important-information-hub", - }, - }); - } - break; - case "getting-started": - { - if((shownTutorials || {})['getting-started'] && !isForce) return - saveShowTutorial('getting-started') + setOpenTutorialModal({ + title: "Important Information!", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "important-information-hub", + poster: importantImg + }, + }); + } + break; + case "getting-started": + { + if((shownTutorials || {})['getting-started'] && !isForce) return + saveShowTutorial('getting-started') - setOpenTutorialModal({ - multi: [ - - { - title: "1. Getting Started", + setOpenTutorialModal({ + multi: [ + + { + title: "1. Getting Started", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "getting-started-hub", + poster: startedImg + }, + }, + { + title: "2. Overview", resource: { name: "a-test", service: "VIDEO", - identifier: "getting-started-hub", + identifier: "overview-hub", + poster: overviewImg }, }, - { - title: "2. Overview", + { + title: "3. Qortal Groups", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "groups-hub", + poster: groupsImg + }, + }, + ], + }); + } + break; + case "qapps": + { + if((shownTutorials || {})['qapps'] && !isForce) return + saveShowTutorial('qapps') + + setOpenTutorialModal({ + multi: [ + { + title: "1. Apps Dashboard", resource: { name: "a-test", service: "VIDEO", - identifier: "overview-hub", + identifier: "apps-dashboard-hub", + poster: dashboardImg }, }, - { - title: "3. Qortal Groups", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "groups-hub", - }, - }, - ], - }); - } - break; - case "qapps": - { - if((shownTutorials || {})['qapps'] && !isForce) return - saveShowTutorial('qapps') - - setOpenTutorialModal({ - multi: [ - { - title: "1. Apps Dashboard", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "apps-dashboard-hub", - }, + { + title: "2. Apps Navigation", + resource: { + name: "a-test", + service: "VIDEO", + identifier: "apps-navigation-hub", + poster: navigationImg }, - { - title: "2. Apps Navigation", - resource: { - name: "a-test", - service: "VIDEO", - identifier: "apps-navigation-hub", - }, - } - - ], - }); - } - break; - default: - break; - } + } + ], + }); + } + break; + default: + break; + } } catch (error) { //error } diff --git a/src/components/WrapperUserAction.tsx b/src/components/WrapperUserAction.tsx index aa0d19a..8337943 100644 --- a/src/components/WrapperUserAction.tsx +++ b/src/components/WrapperUserAction.tsx @@ -80,7 +80,8 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { }, 200); }} sx={{ - color: 'white' + color: 'white', + justifyContent: 'flex-start' }} > Message @@ -98,11 +99,26 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => { }} sx={{ - color: 'white' + color: 'white', + justifyContent: 'flex-start' }} > Send QORT +