added group link, poster, and copy address
@ -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() {
|
||||
}}
|
||||
>
|
||||
<TaskManger getUserInfo={getUserInfo} />
|
||||
<GlobalActions memberGroups={memberGroups} />
|
||||
</MyContext.Provider>
|
||||
)}
|
||||
<Spacer height="20px" />
|
||||
|
@ -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;
|
||||
|
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 { 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",
|
||||
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",
|
||||
poster: importantImg
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "getting-started":
|
||||
{
|
||||
if((shownTutorials || {})['getting-started'] && !isForce) return
|
||||
saveShowTutorial('getting-started')
|
||||
|
||||
setOpenTutorialModal({
|
||||
multi: [
|
||||
|
||||
{
|
||||
title: "1. Getting Started",
|
||||
resource: {
|
||||
name: "a-test",
|
||||
service: "VIDEO",
|
||||
identifier: "getting-started-hub",
|
||||
poster: startedImg
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
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({
|
||||
multi: [
|
||||
|
||||
{
|
||||
title: "1. Getting Started",
|
||||
{
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(address|| "");
|
||||
handleClose();
|
||||
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
Copy address
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
|