added group link, poster, and copy address
@ -124,6 +124,7 @@ import { useHandleUserInfo } from "./components/Group/useHandleUserInfo";
|
|||||||
import { Minting } from "./components/Minting/Minting";
|
import { Minting } from "./components/Minting/Minting";
|
||||||
import { isRunningGateway } from "./qortalRequests";
|
import { isRunningGateway } from "./qortalRequests";
|
||||||
import { QMailStatus } from "./components/QMailStatus";
|
import { QMailStatus } from "./components/QMailStatus";
|
||||||
|
import { GlobalActions } from "./components/GlobalActions/GlobalActions";
|
||||||
|
|
||||||
type extStates =
|
type extStates =
|
||||||
| "not-authenticated"
|
| "not-authenticated"
|
||||||
@ -1686,6 +1687,7 @@ function App() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TaskManger getUserInfo={getUserInfo} />
|
<TaskManger getUserInfo={getUserInfo} />
|
||||||
|
<GlobalActions memberGroups={memberGroups} />
|
||||||
</MyContext.Provider>
|
</MyContext.Provider>
|
||||||
)}
|
)}
|
||||||
<Spacer height="20px" />
|
<Spacer height="20px" />
|
||||||
|
@ -97,6 +97,28 @@ export const MessageDisplay = ({ htmlContent, isReply }) => {
|
|||||||
window.electronAPI.openExternal(href);
|
window.electronAPI.openExternal(href);
|
||||||
} else if (target.getAttribute('data-url')) {
|
} else if (target.getAttribute('data-url')) {
|
||||||
const url = 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);
|
const res = extractComponents(url);
|
||||||
if (res) {
|
if (res) {
|
||||||
const { service, name, identifier, path } = res;
|
const { service, name, identifier, path } = res;
|
||||||
|
10
src/components/GlobalActions/GlobalActions.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { JoinGroup } from './JoinGroup'
|
||||||
|
|
||||||
|
export const GlobalActions = ({memberGroups}) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JoinGroup memberGroups={memberGroups} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
272
src/components/GlobalActions/JoinGroup.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
{!groupInfo && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "325px",
|
||||||
|
height: "150px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<CircularProgress
|
||||||
|
size={25}
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>{" "}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "325px",
|
||||||
|
height: "auto",
|
||||||
|
maxHeight: "400px",
|
||||||
|
display: !groupInfo ? "none" : "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "15px",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Group name: {` ${groupInfo?.groupName}`}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "15px",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Number of members: {` ${groupInfo?.memberCount}`}
|
||||||
|
</Typography>
|
||||||
|
{groupInfo?.description && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "15px",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{groupInfo?.description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{isInGroup && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
*You are already in this group!
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{!isInGroup && groupInfo?.isOpen === false && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
*This is a closed/private group, so you will need to wait until
|
||||||
|
an admin accepts your request
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<ButtonBase onClick={() => {
|
||||||
|
joinGroup(groupInfo, groupInfo?.isOpen);
|
||||||
|
|
||||||
|
setIsOpen(false);
|
||||||
|
}} disabled={isInGroup}>
|
||||||
|
<CustomButtonAccept
|
||||||
|
color="black"
|
||||||
|
bgColor="var(--green)"
|
||||||
|
sx={{
|
||||||
|
minWidth: "102px",
|
||||||
|
height: "45px",
|
||||||
|
fontSize: '16px',
|
||||||
|
opacity: isInGroup ? 0.1 : 1
|
||||||
|
}}
|
||||||
|
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</CustomButtonAccept>
|
||||||
|
</ButtonBase>
|
||||||
|
|
||||||
|
<CustomButtonAccept
|
||||||
|
color="black"
|
||||||
|
bgColor="var(--danger)"
|
||||||
|
sx={{
|
||||||
|
minWidth: "102px",
|
||||||
|
height: "45px",
|
||||||
|
}}
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</CustomButtonAccept>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<CustomizedSnackbars
|
||||||
|
open={openSnack}
|
||||||
|
setOpen={setOpenSnack}
|
||||||
|
info={infoSnack}
|
||||||
|
setInfo={setInfoSnack}
|
||||||
|
/>
|
||||||
|
{isLoadingJoinGroup && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FidgetSpinner
|
||||||
|
visible={true}
|
||||||
|
height="80"
|
||||||
|
width="80"
|
||||||
|
ariaLabel="fidget-spinner-loading"
|
||||||
|
wrapperStyle={{}}
|
||||||
|
wrapperClass="fidget-spinner-wrapper"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
BIN
src/components/Tutorials/img/creation.webp
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/components/Tutorials/img/dashboard.webp
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
src/components/Tutorials/img/groups.webp
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
src/components/Tutorials/img/important.webp
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
src/components/Tutorials/img/navigation.webp
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/components/Tutorials/img/overview.webp
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/components/Tutorials/img/started.webp
Normal file
After Width: | Height: | Size: 19 KiB |
@ -1,6 +1,12 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { saveToLocalStorage } from "../Apps/AppsNavBar";
|
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 () => {
|
const checkIfGatewayIsOnline = async () => {
|
||||||
try {
|
try {
|
||||||
@ -61,102 +67,108 @@ useEffect(()=> {
|
|||||||
const isOnline = await checkIfGatewayIsOnline()
|
const isOnline = await checkIfGatewayIsOnline()
|
||||||
if(!isOnline) return
|
if(!isOnline) return
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "create-account":
|
case "create-account":
|
||||||
{
|
{
|
||||||
if((shownTutorials || {})['create-account'] && !isForce) return
|
if((shownTutorials || {})['create-account'] && !isForce) return
|
||||||
saveShowTutorial('create-account')
|
saveShowTutorial('create-account')
|
||||||
setOpenTutorialModal({
|
setOpenTutorialModal({
|
||||||
title: "Account Creation",
|
title: "Account Creation",
|
||||||
resource: {
|
resource: {
|
||||||
name: "a-test",
|
name: "a-test",
|
||||||
service: "VIDEO",
|
service: "VIDEO",
|
||||||
identifier: "account-creation-hub",
|
identifier: "account-creation-hub",
|
||||||
},
|
poster: creationImg
|
||||||
});
|
},
|
||||||
}
|
});
|
||||||
break;
|
}
|
||||||
case "important-information":
|
break;
|
||||||
{
|
case "important-information":
|
||||||
if((shownTutorials || {})['important-information'] && !isForce) return
|
{
|
||||||
saveShowTutorial('important-information')
|
if((shownTutorials || {})['important-information'] && !isForce) return
|
||||||
|
saveShowTutorial('important-information')
|
||||||
|
|
||||||
setOpenTutorialModal({
|
setOpenTutorialModal({
|
||||||
title: "Important Information!",
|
title: "Important Information!",
|
||||||
resource: {
|
resource: {
|
||||||
name: "a-test",
|
name: "a-test",
|
||||||
service: "VIDEO",
|
service: "VIDEO",
|
||||||
identifier: "important-information-hub",
|
identifier: "important-information-hub",
|
||||||
},
|
poster: importantImg
|
||||||
});
|
},
|
||||||
}
|
});
|
||||||
break;
|
}
|
||||||
case "getting-started":
|
break;
|
||||||
{
|
case "getting-started":
|
||||||
if((shownTutorials || {})['getting-started'] && !isForce) return
|
{
|
||||||
saveShowTutorial('getting-started')
|
if((shownTutorials || {})['getting-started'] && !isForce) return
|
||||||
|
saveShowTutorial('getting-started')
|
||||||
|
|
||||||
setOpenTutorialModal({
|
setOpenTutorialModal({
|
||||||
multi: [
|
multi: [
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "1. Getting Started",
|
title: "1. Getting Started",
|
||||||
|
resource: {
|
||||||
|
name: "a-test",
|
||||||
|
service: "VIDEO",
|
||||||
|
identifier: "getting-started-hub",
|
||||||
|
poster: startedImg
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2. Overview",
|
||||||
resource: {
|
resource: {
|
||||||
name: "a-test",
|
name: "a-test",
|
||||||
service: "VIDEO",
|
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: {
|
resource: {
|
||||||
name: "a-test",
|
name: "a-test",
|
||||||
service: "VIDEO",
|
service: "VIDEO",
|
||||||
identifier: "overview-hub",
|
identifier: "apps-dashboard-hub",
|
||||||
|
poster: dashboardImg
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "3. Qortal Groups",
|
title: "2. Apps Navigation",
|
||||||
resource: {
|
resource: {
|
||||||
name: "a-test",
|
name: "a-test",
|
||||||
service: "VIDEO",
|
service: "VIDEO",
|
||||||
identifier: "groups-hub",
|
identifier: "apps-navigation-hub",
|
||||||
},
|
poster: navigationImg
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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",
|
break;
|
||||||
identifier: "apps-navigation-hub",
|
default:
|
||||||
},
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//error
|
//error
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,8 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
|
|||||||
}, 200);
|
}, 200);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'white'
|
color: 'white',
|
||||||
|
justifyContent: 'flex-start'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Message
|
Message
|
||||||
@ -98,11 +99,26 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
|
|||||||
|
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
color: 'white'
|
color: 'white',
|
||||||
|
justifyContent: 'flex-start'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Send QORT
|
Send QORT
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(address|| "");
|
||||||
|
handleClose();
|
||||||
|
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
color: 'white',
|
||||||
|
justifyContent: 'flex-start'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy address
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Popover>
|
</Popover>
|
||||||
</>
|
</>
|
||||||
|