added embeds and q-manager for groups

This commit is contained in:
PhilReact 2024-11-30 13:13:00 +02:00
parent 9f12aa4813
commit 6d416dc5e9
22 changed files with 1885 additions and 844 deletions

View File

@ -44,7 +44,7 @@ import Info from "./assets/svgs/Info.svg";
import CloseIcon from "@mui/icons-material/Close";
import { FilePicker } from '@capawesome/capacitor-file-picker';
import './utils/seedPhrase/RandomSentenceGenerator';
import { useFetchResources } from "./common/useFetchResources";
import {
createAccount,
generateRandomSentence,
@ -348,6 +348,7 @@ export const isMainWindow = true;
function App() {
const [extState, setExtstate] = useState<extStates>("not-authenticated");
const [desktopViewMode, setDesktopViewMode] = useState("home");
const {downloadResource} = useFetchResources()
const [backupjson, setBackupjson] = useState<any>(null);
const [rawWallet, setRawWallet] = useState<any>(null);
@ -1682,7 +1683,8 @@ function App() {
setOpenSnackGlobal: setOpenSnack,
infoSnackCustom: infoSnack,
setInfoSnackCustom: setInfoSnack,
userInfo: userInfo
userInfo: userInfo,
downloadResource
}}
>
<Box

View File

@ -1,4 +1,4 @@
import { atom } from 'recoil';
import { atom, selectorFamily } from 'recoil';
export const sortablePinnedAppsAtom = atom({
@ -99,4 +99,30 @@ export const isFocusedParentGroupAtom = atom({
export const isFocusedParentDirectAtom = atom({
key: 'isFocusedParentDirectAtom',
default: false,
});
});
export const resourceDownloadControllerAtom = atom({
key: 'resourceDownloadControllerAtom',
default: {},
});
export const resourceKeySelector = selectorFamily({
key: 'resourceKeySelector',
get: (key) => ({ get }) => {
const resources = get(resourceDownloadControllerAtom);
return resources[key] || null; // Return the value for the key or null if not found
},
});
export const blobControllerAtom = atom({
key: 'blobControllerAtom',
default: {},
});
export const blobKeySelector = selectorFamily({
key: 'blobKeySelector',
get: (key) => ({ get }) => {
const blobs = get(blobControllerAtom);
return blobs[key] || null; // Return the value for the key or null if not found
},
});

View File

@ -1220,6 +1220,7 @@ export async function encryptAndPublishSymmetricKeyGroupChatCase(
},
event.origin
);
if (!previousData) {
try {
sendChatGroup({
groupId,
@ -1230,6 +1231,7 @@ export async function encryptAndPublishSymmetricKeyGroupChatCase(
} catch (error) {
// error in sending chat message
}
}
try {
sendChatNotification(data, groupId, previousData, numberOfMembers);
} catch (error) {

View File

@ -122,6 +122,9 @@ FilePicker.requestPermissions().then(permission => {
}).catch((error)=> console.error(error));;
export let groupSecretkeys = {}
export function cleanUrl(url) {
return url?.replace(/^(https?:\/\/)?(www\.)?/, "");
@ -2888,7 +2891,7 @@ function setupMessageListener() {
// for announcement notification
clearInterval(interval);
}
groupSecretkeys = {}
const wallet = await getSaveWallet();
const address = wallet.address0;
const key1 = `tempPublish-${address}`;

View File

@ -0,0 +1,124 @@
import React, { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { resourceDownloadControllerAtom } from '../atoms/global';
import { getBaseApiReact } from '../App';
export const useFetchResources = () => {
const [resources, setResources] = useRecoilState(resourceDownloadControllerAtom);
const downloadResource = useCallback(({ service, name, identifier }, build) => {
setResources((prev) => ({
...prev,
[`${service}-${name}-${identifier}`]: {
...(prev[`${service}-${name}-${identifier}`] || {}),
service,
name,
identifier,
},
}));
try {
let isCalling = false;
let percentLoaded = 0;
let timer = 24;
let calledFirstTime = false
const intervalId = setInterval(async () => {
if (isCalling) return;
isCalling = true;
let res
if(!build){
const urlFirstTime = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}`;
const resCall = await fetch(urlFirstTime, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
res = await resCall.json()
}
if(build || (calledFirstTime === false && res?.status !== 'READY')){
const url = `${getBaseApiReact()}/arbitrary/resource/status/${service}/${name}/${identifier}?build=true`;
const resCall = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
res = await resCall.json();
}
calledFirstTime = true
isCalling = false;
if (res.localChunkCount) {
if (res.percentLoaded) {
if (res.percentLoaded === percentLoaded && res.percentLoaded !== 100) {
timer = timer - 5;
} else {
timer = 24;
}
if (timer < 0) {
timer = 24;
isCalling = true;
// Update Recoil state for refetching
setResources((prev) => ({
...prev,
[`${service}-${name}-${identifier}`]: {
...(prev[`${service}-${name}-${identifier}`] || {}),
status: {
...res,
status: 'REFETCHING',
},
},
}));
setTimeout(() => {
isCalling = false;
downloadResource({ name, service, identifier }, true);
}, 25000);
return;
}
percentLoaded = res.percentLoaded;
}
// Update Recoil state for progress
setResources((prev) => ({
...prev,
[`${service}-${name}-${identifier}`]: {
...(prev[`${service}-${name}-${identifier}`] || {}),
status: res,
},
}));
}
// Check if progress is 100% and clear interval if true
if (res?.status === 'READY') {
clearInterval(intervalId);
// Update Recoil state for completion
setResources((prev) => ({
...prev,
[`${service}-${name}-${identifier}`]: {
...(prev[`${service}-${name}-${identifier}`] || {}),
status: res,
},
}));
}
}, !calledFirstTime ? 100 :5000);
} catch (error) {
console.error('Error during resource fetch:', error);
}
}, [setResources]);
return { downloadResource };
};

View File

@ -3,7 +3,7 @@ import { AppViewer } from './AppViewer';
import Frame from 'react-frame-component';
import { MyContext, isMobile } from '../../App';
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) => {
const AppViewerContainer = React.forwardRef(({ app, isSelected, hide, customHeight }, ref) => {
const { rootHeight } = useContext(MyContext);
@ -36,7 +36,7 @@ const AppViewerContainer = React.forwardRef(({ app, isSelected, hide }, ref) =>
}
style={{
display: (!isSelected || hide) && 'none',
height: !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
height: customHeight ? customHeight : !isMobile ? '100vh' : `calc(${rootHeight} - 60px - 45px)`,
border: 'none',
width: '100%',
overflow: 'hidden',

View File

@ -77,9 +77,8 @@ export const saveFileInChunks = async (
isFirstChunk = false;
}
console.log('File saved successfully in chunks:', fullFileName);
} catch (error) {
console.error('Error saving file in chunks:', error);
throw error
}
};
@ -184,7 +183,7 @@ const UIQortalRequests = [
'GET_TX_ACTIVITY_SUMMARY', 'GET_FOREIGN_FEE', 'UPDATE_FOREIGN_FEE',
'GET_SERVER_CONNECTION_HISTORY', 'SET_CURRENT_FOREIGN_SERVER',
'ADD_FOREIGN_SERVER', 'REMOVE_FOREIGN_SERVER', 'GET_DAY_SUMMARY', 'CREATE_TRADE_BUY_ORDER',
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK'
'CREATE_TRADE_SELL_ORDER', 'CANCEL_TRADE_SELL_ORDER', 'IS_USING_GATEWAY', 'ADMIN_ACTION', 'OPEN_NEW_TAB', 'CREATE_AND_COPY_EMBED_LINK', 'DECRYPT_QORTAL_GROUP_DATA'
];
@ -282,6 +281,7 @@ const UIQortalRequests = [
setOpenSnackGlobal(true);
await saveFileInChunks(blob, filename)
setInfoSnackCustom({
type: "success",
message:
@ -479,7 +479,7 @@ isDOMContentLoaded: false
} else if (
event?.data?.action === 'PUBLISH_MULTIPLE_QDN_RESOURCES' ||
event?.data?.action === 'PUBLISH_QDN_RESOURCE' ||
event?.data?.action === 'ENCRYPT_DATA'
event?.data?.action === 'ENCRYPT_DATA' || event?.data?.action === 'ENCRYPT_DATA_WITH_SHARING_KEY' || 'ENCRYPT_QORTAL_GROUP_DATA'
) {
let data;

View File

@ -15,7 +15,7 @@ import { CustomizedSnackbars } from '../Snackbar/Snackbar'
import { PUBLIC_NOTIFICATION_CODE_FIRST_SECRET_KEY } from '../../constants/codes'
import { useMessageQueue } from '../../MessageQueueContext'
import { executeEvent } from '../../utils/events'
import { Box, ButtonBase, Typography } from '@mui/material'
import { Box, ButtonBase, Divider, Typography } from '@mui/material'
import ShortUniqueId from "short-unique-id";
import { ReplyPreview } from './MessageItem'
import { ExitIcon } from '../../assets/Icons/ExitIcon'
@ -25,6 +25,8 @@ import MentionList from './MentionList'
import { ChatOptions } from './ChatOptions'
import { isFocusedParentGroupAtom } from '../../atoms/global'
import { useRecoilState } from 'recoil'
import AppViewerContainer from '../Apps/AppViewerContainer'
import CloseIcon from "@mui/icons-material/Close";
const uid = new ShortUniqueId({ length: 5 });
@ -35,7 +37,7 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const [isLoading, setIsLoading] = useState(false)
const [messageSize, setMessageSize] = useState(0)
const [onEditMessage, setOnEditMessage] = useState(null)
const [isOpenQManager, setIsOpenQManager] = useState(null)
const [isMoved, setIsMoved] = useState(false);
const [openSnack, setOpenSnack] = React.useState(false);
const [infoSnack, setInfoSnack] = React.useState(null);
@ -56,7 +58,9 @@ export const ChatGroup = ({selectedGroup, secretKey, setSecretKey, getSecretKey,
const lastReadTimestamp = useRef(null)
const openQManager = useCallback(()=> {
setIsOpenQManager(true)
}, [])
const getTimestampEnterChat = async () => {
try {
return new Promise((res, rej) => {
@ -826,7 +830,8 @@ const sendMessage = async ()=> {
<div style={{
display: isFocusedParent ? 'none' : 'block'
}}>
<ChatOptions messages={messages} goToMessage={()=> {}} members={members} myName={myName} selectedGroup={selectedGroup}/>
<ChatOptions openQManager={openQManager}
messages={messages} goToMessage={()=> {}} members={members} myName={myName} selectedGroup={selectedGroup}/>
</div>
</Box>
@ -897,7 +902,55 @@ const sendMessage = async ()=> {
{` Send`}
</CustomButton>
)}
{isOpenQManager !== null && (
<Box sx={{
position: 'fixed',
height: '100vh',
maxHeight: '100vh',
width: '400px',
maxWidth: '100vw',
backgroundColor: '#27282c',
zIndex: 100,
top: 0,
bottom: 0,
right: 0,
overflow: 'hidden',
borderTopLeftRadius: '10px',
borderTopRightRadius: '10px',
display: isOpenQManager === true ? 'block' : 'none',
boxShadow: 4,
}}>
<Box sx={{
height: '100%',
width: '100%',
}}>
<Box sx={{
height: '40px',
display: 'flex',
alignItems: 'center',
padding: '5px',
justifyContent: 'space-between'
}}>
<Typography>Q-Manager</Typography>
<ButtonBase onClick={()=> {
setIsOpenQManager(false)
}}><CloseIcon sx={{
color: 'white'
}} /></ButtonBase>
</Box>
<Divider />
<AppViewerContainer app={{
tabId: '5558588',
name: 'Q-Manager',
service: 'APP'
}} isSelected />
</Box>
</Box>
)}
</Box>
{isFocusedParent && messageSize > 750 && (

View File

@ -32,6 +32,7 @@ import { formatTimestamp } from "../../utils/time";
import { ContextMenuMentions } from "../ContextMenuMentions";
import { convert } from 'html-to-text';
import { executeEvent } from "../../utils/events";
import InsertLinkIcon from '@mui/icons-material/InsertLink';
const extractTextFromHTML = (htmlString = '') => {
return convert(htmlString, {
@ -43,7 +44,7 @@ const cache = new CellMeasurerCache({
defaultHeight: 50,
});
export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGroup }) => {
export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGroup, openQManager }) => {
const [mode, setMode] = useState("default");
const [searchValue, setSearchValue] = useState("");
const [selectedMember, setSelectedMember] = useState(0);
@ -712,6 +713,16 @@ export const ChatOptions = ({ messages, goToMessage, members, myName, selectedGr
}} />
</ButtonBase>
</ContextMenuMentions>
<ButtonBase onClick={() => {
setMode("default")
setSearchValue('')
setSelectedMember(0)
openQManager()
}}>
<InsertLinkIcon sx={{
color: 'white'
}} />
</ButtonBase>
</Box>
</Box>
);

View File

@ -0,0 +1,329 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { MyContext, getBaseApiReact } from "../../App";
import {
Card,
CardContent,
CardHeader,
Typography,
RadioGroup,
Radio,
FormControlLabel,
Button,
Box,
ButtonBase,
Divider,
Dialog,
IconButton,
CircularProgress,
} from "@mui/material";
import { base64ToBlobUrl } from "../../utils/fileReading";
import { saveFileToDiskGeneric } from "../../utils/generateWallet/generateWallet";
import AttachmentIcon from '@mui/icons-material/Attachment';
import RefreshIcon from "@mui/icons-material/Refresh";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import { CustomLoader } from "../../common/CustomLoader";
import { Spacer } from "../../common/Spacer";
import { FileAttachmentContainer, FileAttachmentFont } from "./Embed-styles";
import DownloadIcon from "@mui/icons-material/Download";
import SaveIcon from '@mui/icons-material/Save';
import { useSetRecoilState } from "recoil";
import { blobControllerAtom } from "../../atoms/global";
export const AttachmentCard = ({
resourceData,
resourceDetails,
owner,
refresh,
openExternal,
external,
isLoadingParent,
errorMsg,
encryptionType,
setInfoSnack,
setOpenSnack
}) => {
const [isOpen, setIsOpen] = useState(true);
const { downloadResource } = useContext(MyContext);
const saveToDisk = async ()=> {
const { name, service, identifier } = resourceData;
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`;
fetch(url)
.then(response => response.blob())
.then(async blob => {
setOpenSnack(true)
setInfoSnack({
type: "info",
message:
"Saving file...",
});
await saveFileToDiskGeneric(blob, resourceData?.fileName)
setOpenSnack(true)
setInfoSnack({
type: "success",
message:
"File saved in INTERNAL STORAGE, DOCUMENT folder.",
});
})
.catch(error => {
console.error("Error fetching the video:", error);
});
}
const saveToDiskEncrypted = async ()=> {
let blobUrl
try {
const { name, service, identifier,key } = resourceData;
const url = `${getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}?encoding=base64`;
const res = await fetch(url)
const data = await res.text();
let decryptedData
try {
if(key && encryptionType === 'private'){
decryptedData = await window.sendMessage(
"DECRYPT_DATA_WITH_SHARING_KEY",
{
encryptedData: data,
key: decodeURIComponent(key),
}
);
}
if(encryptionType === 'group'){
decryptedData = await window.sendMessage(
"DECRYPT_QORTAL_GROUP_DATA",
{
data64: data,
groupId: 683,
}
);
}
} catch (error) {
throw new Error('Unable to decrypt')
}
if (!decryptedData || decryptedData?.error) throw new Error("Could not decrypt data");
blobUrl = base64ToBlobUrl(decryptedData, resourceData?.mimeType)
const response = await fetch(blobUrl);
const blob = await response.blob();
setOpenSnack(true)
setInfoSnack({
type: "info",
message:
"Saving file...",
});
await saveFileToDiskGeneric(blob, resourceData?.fileName)
setOpenSnack(true)
setInfoSnack({
type: "success",
message:
"File saved in INTERNAL STORAGE, DOCUMENT folder.",
});
} catch (error) {
console.error(error)
} finally {
if(blobUrl){
URL.revokeObjectURL(blobUrl);
}
}
}
return (
<Card
sx={{
backgroundColor: "#1F2023",
height: "250px",
// height: isOpen ? "auto" : "150px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "16px 16px 0px 16px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<AttachmentIcon
sx={{
color: "white",
}}
/>
<Typography>ATTACHMENT embed</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<ButtonBase>
<RefreshIcon
onClick={refresh}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
{external && (
<ButtonBase>
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
)}
</Box>
</Box>
<Box
sx={{
padding: "8px 16px 8px 16px",
}}
>
<Typography
sx={{
fontSize: "12px",
color: "white",
}}
>
Created by {owner}
</Typography>
<Typography
sx={{
fontSize: "12px",
color: "cadetblue",
}}
>
{encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"}
</Typography>
</Box>
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: "center",
}}
>
{isLoadingParent && isOpen && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<CustomLoader />{" "}
</Box>
)}
{errorMsg && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<Typography
sx={{
fontSize: "14px",
color: "var(--unread)",
}}
>
{errorMsg}
</Typography>{" "}
</Box>
)}
</Box>
<Box>
<CardContent>
{resourceData?.fileName && (
<>
<Typography sx={{
fontSize: '14px'
}}>{resourceData?.fileName}</Typography>
<Spacer height="10px" />
</>
)}
<ButtonBase sx={{
width: '90%',
maxWidth: '400px'
}} onClick={()=> {
if(resourceDetails?.status?.status === 'READY'){
if(encryptionType){
saveToDiskEncrypted()
return
}
saveToDisk()
return
}
downloadResource(resourceData)
}}>
<FileAttachmentContainer >
{!resourceDetails && (
<>
<DownloadIcon />
<FileAttachmentFont sx={{
fontSize: '14px'
}}>Download File</FileAttachmentFont>
</>
)}
{resourceDetails && resourceDetails?.status?.status !== 'READY' && (
<>
<CircularProgress sx={{
color: 'white'
}} size={20} />
<FileAttachmentFont sx={{
fontSize: '14px'
}}>Downloading: {resourceDetails?.status?.percentLoaded || '0'}%</FileAttachmentFont>
</>
)}
{resourceDetails && resourceDetails?.status?.status === 'READY' && (
<>
<SaveIcon />
<FileAttachmentFont sx={{
fontSize: '14px'
}}>Save to Disk</FileAttachmentFont>
</>
)}
</FileAttachmentContainer>
</ButtonBase>
<Typography sx={{
fontSize: '14px'
}}>{resourceDetails?.status?.status}</Typography>
</CardContent>
</Box>
</Card>
);
};

View File

@ -0,0 +1,18 @@
import { Box, Typography, styled } from "@mui/material";
export const FileAttachmentContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
padding: "5px 10px",
border: `1px solid ${theme.palette.text.primary}`,
width: "100%",
gap: '20px'
}));
export const FileAttachmentFont = styled(Typography)(({ theme }) => ({
fontSize: "20px",
letterSpacing: 0,
fontWeight: 400,
userSelect: "none",
whiteSpace: "nowrap",
}));

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,264 @@
import React, { useEffect, useState } from "react";
import {
Card,
CardContent,
Typography,
Box,
ButtonBase,
Divider,
Dialog,
IconButton,
} from "@mui/material";
import RefreshIcon from "@mui/icons-material/Refresh";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
import { CustomLoader } from "../../common/CustomLoader";
import ImageIcon from "@mui/icons-material/Image";
import CloseIcon from "@mui/icons-material/Close";
export const ImageCard = ({
image,
fetchImage,
owner,
refresh,
openExternal,
external,
isLoadingParent,
errorMsg,
encryptionType,
}) => {
const [isOpen, setIsOpen] = useState(true);
const [height, setHeight] = useState('400px')
useEffect(() => {
if (isOpen) {
fetchImage();
}
}, [isOpen]);
useEffect(()=> {
if(errorMsg){
setHeight('300px')
}
}, [errorMsg])
return (
<Card
sx={{
backgroundColor: "#1F2023",
height: height,
transition: "height 0.6s ease-in-out",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "16px 16px 0px 16px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<ImageIcon
sx={{
color: "white",
}}
/>
<Typography>IMAGE embed</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<ButtonBase>
<RefreshIcon
onClick={refresh}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
{external && (
<ButtonBase>
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
)}
</Box>
</Box>
<Box
sx={{
padding: "8px 16px 8px 16px",
}}
>
<Typography
sx={{
fontSize: "12px",
color: "white",
}}
>
Created by {owner}
</Typography>
<Typography
sx={{
fontSize: "12px",
color: "cadetblue",
}}
>
{encryptionType === 'private' ? "ENCRYPTED" : encryptionType === 'group' ? 'GROUP ENCRYPTED' : "Not encrypted"}
</Typography>
</Box>
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: "center",
}}
>
{isLoadingParent && isOpen && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<CustomLoader />{" "}
</Box>
)}
{errorMsg && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<Typography
sx={{
fontSize: "14px",
color: "var(--unread)",
}}
>
{errorMsg}
</Typography>{" "}
</Box>
)}
</Box>
<Box>
<CardContent>
<ImageViewer src={image} />
</CardContent>
</Box>
</Card>
);
};
export function ImageViewer({ src, alt = "" }) {
const [isFullscreen, setIsFullscreen] = useState(false);
const handleOpenFullscreen = () => setIsFullscreen(true);
const handleCloseFullscreen = () => setIsFullscreen(false);
return (
<>
{/* Image in container */}
<Box
sx={{
maxWidth: "100%", // Prevent horizontal overflow
display: "flex",
justifyContent: "center",
cursor: "pointer",
}}
onClick={handleOpenFullscreen}
>
<img
src={src}
alt={alt}
style={{
maxWidth: "100%",
maxHeight: "450px", // Adjust max height for small containers
objectFit: "contain", // Preserve aspect ratio
}}
/>
</Box>
{/* Fullscreen Viewer */}
<Dialog
open={isFullscreen}
onClose={handleCloseFullscreen}
maxWidth="lg"
fullWidth
fullScreen
sx={{
"& .MuiDialog-paper": {
margin: 0,
maxWidth: "100%",
width: "100%",
height: "100vh",
overflow: "hidden", // Prevent scrollbars
},
}}
>
<Box
sx={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#000", // Optional: dark background for fullscreen mode
}}
>
{/* Close Button */}
<IconButton
onClick={handleCloseFullscreen}
sx={{
position: "absolute",
top: 8,
right: 8,
zIndex: 10,
color: "white",
}}
>
<CloseIcon />
</IconButton>
{/* Fullscreen Image */}
<img
src={src}
alt={alt}
style={{
maxWidth: "100%",
maxHeight: "100%",
objectFit: "contain", // Preserve aspect ratio
}}
/>
</Box>
</Dialog>
</>
);
}

View File

@ -0,0 +1,388 @@
import React, { useContext, useEffect, useState } from "react";
import { MyContext } from "../../App";
import {
Card,
CardContent,
CardHeader,
Typography,
RadioGroup,
Radio,
FormControlLabel,
Button,
Box,
ButtonBase,
Divider,
} from "@mui/material";
import { getNameInfo } from "../Group/Group";
import PollIcon from "@mui/icons-material/Poll";
import { getFee } from "../../background";
import RefreshIcon from "@mui/icons-material/Refresh";
import { Spacer } from "../../common/Spacer";
import OpenInNewIcon from "@mui/icons-material/OpenInNew";
export const PollCard = ({
poll,
setInfoSnack,
setOpenSnack,
refresh,
openExternal,
external,
isLoadingParent,
errorMsg,
}) => {
const [selectedOption, setSelectedOption] = useState("");
const [ownerName, setOwnerName] = useState("");
const [showResults, setShowResults] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const { show, userInfo } = useContext(MyContext);
const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
const handleVote = async () => {
const fee = await getFee("VOTE_ON_POLL");
await show({
message: `Do you accept this VOTE_ON_POLL transaction? POLLS are public!`,
publishFee: fee.fee + " QORT",
});
setIsLoadingSubmit(true);
window
.sendMessage(
"voteOnPoll",
{
pollName: poll?.info?.pollName,
optionIndex: +selectedOption,
},
60000
)
.then((response) => {
setIsLoadingSubmit(false);
if (response.error) {
setInfoSnack({
type: "error",
message: response?.error || "Unable to vote.",
});
setOpenSnack(true);
return;
} else {
setInfoSnack({
type: "success",
message:
"Successfully voted. Please wait a couple minutes for the network to propogate the changes.",
});
setOpenSnack(true);
}
})
.catch((error) => {
setIsLoadingSubmit(false);
setInfoSnack({
type: "error",
message: error?.message || "Unable to vote.",
});
setOpenSnack(true);
});
};
const getName = async (owner) => {
try {
const res = await getNameInfo(owner);
if (res) {
setOwnerName(res);
}
} catch (error) {}
};
useEffect(() => {
if (poll?.info?.owner) {
getName(poll.info.owner);
}
}, [poll?.info?.owner]);
return (
<Card
sx={{
backgroundColor: "#1F2023",
height: isOpen ? "auto" : "150px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "16px 16px 0px 16px",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<PollIcon
sx={{
color: "white",
}}
/>
<Typography>POLL embed</Typography>
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "10px",
}}
>
<ButtonBase>
<RefreshIcon
onClick={refresh}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
{external && (
<ButtonBase>
<OpenInNewIcon
onClick={openExternal}
sx={{
fontSize: "24px",
color: "white",
}}
/>
</ButtonBase>
)}
</Box>
</Box>
<Box
sx={{
padding: "8px 16px 8px 16px",
}}
>
<Typography
sx={{
fontSize: "12px",
}}
>
Created by {ownerName || poll?.info?.owner}
</Typography>
</Box>
<Divider sx={{ borderColor: "rgb(255 255 255 / 10%)" }} />
<Box
sx={{
display: "flex",
flexDirection: "column",
width: "100%",
alignItems: "center",
}}
>
{!isOpen && !errorMsg && (
<>
<Spacer height="5px" />
<Button
size="small"
variant="contained"
sx={{
backgroundColor: "var(--green)",
}}
onClick={() => {
setIsOpen(true);
}}
>
Show poll
</Button>
</>
)}
{isLoadingParent && isOpen && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<CustomLoader />{" "}
</Box>
)}
{errorMsg && (
<Box
sx={{
width: "100%",
display: "flex",
justifyContent: "center",
}}
>
{" "}
<Typography
sx={{
fontSize: "14px",
color: "var(--unread)",
}}
>
{errorMsg}
</Typography>{" "}
</Box>
)}
</Box>
<Box
sx={{
display: isOpen ? "block" : "none",
}}
>
<CardHeader
title={poll?.info?.pollName}
subheader={poll?.info?.description}
sx={{
"& .MuiCardHeader-title": {
fontSize: "18px", // Custom font size for title
},
}}
/>
<CardContent>
<Typography
sx={{
fontSize: "18px",
}}
>
Options
</Typography>
<RadioGroup
value={selectedOption}
onChange={(e) => setSelectedOption(e.target.value)}
>
{poll?.info?.pollOptions?.map((option, index) => (
<FormControlLabel
key={index}
value={index}
control={
<Radio
sx={{
color: "white", // Unchecked color
"&.Mui-checked": {
color: "var(--green)", // Checked color
},
}}
/>
}
label={option?.optionName}
/>
))}
</RadioGroup>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "20px",
}}
>
<Button
variant="contained"
color="primary"
disabled={!selectedOption || isLoadingSubmit}
onClick={handleVote}
>
Vote
</Button>
<Typography
sx={{
fontSize: "14px",
fontStyle: "italic",
}}
>
{" "}
{`${poll?.votes?.totalVotes} ${
poll?.votes?.totalVotes === 1 ? " vote" : " votes"
}`}
</Typography>
</Box>
<Spacer height="10px" />
<Typography
sx={{
fontSize: "14px",
visibility: poll?.votes?.votes?.find(
(item) => item?.voterPublicKey === userInfo?.publicKey
)
? "visible"
: "hidden",
}}
>
You've already voted.
</Typography>
<Spacer height="10px" />
{isLoadingSubmit && (
<Typography
sx={{
fontSize: "12px",
}}
>
Is processing transaction, please wait...
</Typography>
)}
<ButtonBase
onClick={() => {
setShowResults((prev) => !prev);
}}
>
{showResults ? "hide " : "show "} results
</ButtonBase>
</CardContent>
{showResults && <PollResults votes={poll?.votes} />}
</Box>
</Card>
);
};
const PollResults = ({ votes }) => {
const maxVotes = Math.max(
...votes?.voteCounts?.map((option) => option.voteCount)
);
const options = votes?.voteCounts;
return (
<Box sx={{ width: "100%", p: 2 }}>
{options
.sort((a, b) => b.voteCount - a.voteCount) // Sort options by votes (highest first)
.map((option, index) => (
<Box key={index} sx={{ mb: 2 }}>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography
variant="body1"
sx={{ fontWeight: index === 0 ? "bold" : "normal" }}
>
{`${index + 1}. ${option.optionName}`}
</Typography>
<Typography
variant="body1"
sx={{ fontWeight: index === 0 ? "bold" : "normal" }}
>
{option.voteCount} votes
</Typography>
</Box>
<Box
sx={{
mt: 1,
height: 10,
backgroundColor: "#e0e0e0",
borderRadius: 5,
overflow: "hidden",
}}
>
<Box
sx={{
width: `${(option.voteCount / maxVotes) * 100}%`,
height: "100%",
backgroundColor: index === 0 ? "#3f51b5" : "#f50057",
transition: "width 0.3s ease-in-out",
}}
/>
</Box>
</Box>
))}
</Box>
);
};

View File

@ -0,0 +1,40 @@
function decodeHTMLEntities(str) {
const txt = document.createElement("textarea");
txt.innerHTML = str;
return txt.value;
}
export const parseQortalLink = (link) => {
const prefix = "qortal://use-embed/";
if (!link.startsWith(prefix)) {
throw new Error("Invalid link format");
}
// Decode any HTML entities in the link
link = decodeHTMLEntities(link);
// Separate the type and query string
const [typePart, queryPart] = link.slice(prefix.length).split("?");
// Ensure only the type is parsed
const type = typePart.split("/")[0].toUpperCase();
const params = {};
if (queryPart) {
const queryPairs = queryPart.split("&");
queryPairs.forEach((pair) => {
const [key, value] = pair.split("=");
if (key && value) {
const decodedKey = decodeURIComponent(key.trim());
const decodedValue = value.trim().replace(
/<\/?[^>]+(>|$)/g,
"" // Remove any HTML tags
);
params[decodedKey] = decodedValue;
}
});
}
return { type, ...params };
};

View File

@ -115,6 +115,38 @@ import { useHandleMobileNativeBack } from "../../hooks/useHandleMobileNativeBack
// }
// });
export const getPublishesFromAdmins = async (admins: string[], groupId) => {
// const validApi = await findUsableApi();
const queryString = admins.map((name) => `name=${name}`).join("&");
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${
groupId
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("network error");
}
const adminData = await response.json();
const filterId = adminData.filter(
(data: any) =>
data.identifier === `symmetric-qchat-group-${groupId}`
);
if (filterId?.length === 0) {
return false;
}
const sortedData = filterId.sort((a: any, b: any) => {
// Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
// Sort by most recent
return dateB.getTime() - dateA.getTime();
});
return sortedData[0];
};
interface GroupProps {
myAddress: string;
isFocused: boolean;
@ -679,36 +711,7 @@ export const Group = ({
// };
// }, [checkGroupListFunc, myAddress]);
const getPublishesFromAdmins = async (admins: string[]) => {
// const validApi = await findUsableApi();
const queryString = admins.map((name) => `name=${name}`).join("&");
const url = `${getBaseApiReact()}${getArbitraryEndpointReact()}?mode=ALL&service=DOCUMENT_PRIVATE&identifier=symmetric-qchat-group-${
selectedGroup?.groupId
}&exactmatchnames=true&limit=0&reverse=true&${queryString}&prefix=true`;
const response = await fetch(url);
if (!response.ok) {
throw new Error("network error");
}
const adminData = await response.json();
const filterId = adminData.filter(
(data: any) =>
data.identifier === `symmetric-qchat-group-${selectedGroup?.groupId}`
);
if (filterId?.length === 0) {
return false;
}
const sortedData = filterId.sort((a: any, b: any) => {
// Get the most recent date for both a and b
const dateA = a.updated ? new Date(a.updated) : new Date(a.created);
const dateB = b.updated ? new Date(b.updated) : new Date(b.created);
// Sort by most recent
return dateB.getTime() - dateA.getTime();
});
return sortedData[0];
};
const getSecretKey = async (
loadingGroupParam?: boolean,
secretKeyToPublish?: boolean
@ -757,7 +760,7 @@ export const Group = ({
throw new Error("Network error");
}
const publish =
publishFromStorage || (await getPublishesFromAdmins(names));
publishFromStorage || (await getPublishesFromAdmins(names, selectedGroup?.groupId));
if (prevGroupId !== selectedGroupRef.current.groupId) {
if (settimeoutForRefetchSecretKey.current) {
@ -2306,6 +2309,7 @@ export const Group = ({
setNewEncryptionNotification
}
hide={groupSection !== "chat" || !secretKey}
handleSecretKeyCreationInProgress={
handleSecretKeyCreationInProgress
}

View File

@ -22,7 +22,9 @@ export const CustomizedSnackbars = ({open, setOpen, info, setInfo, duration}) =
if(!open) return null
return (
<div>
<Snackbar anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={duration || 6000} onClose={handleClose}>
<Snackbar sx={{
zIndex: 15
}} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} open={open} autoHideDuration={duration || 6000} onClose={handleClose}>
<Alert

View File

@ -65,7 +65,7 @@ export const createSymmetricKeyAndNonce = () => {
};
export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey }: any) => {
export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey, customSymmetricKey }: any) => {
let combinedPublicKeys = [...publicKeys, userPublicKey]
const decodedPrivateKey = Base58.decode(privateKey)
@ -76,9 +76,15 @@ export const encryptDataGroup = ({ data64, publicKeys, privateKey, userPublicKey
throw new Error("The Uint8ArrayData you've submitted is invalid")
}
try {
// Generate a random symmetric key for the message.
const messageKey = new Uint8Array(32)
let messageKey
if(customSymmetricKey){
messageKey = base64ToUint8Array(customSymmetricKey)
} else {
messageKey = new Uint8Array(32)
crypto.getRandomValues(messageKey)
}
if(!messageKey) throw new Error('Cannot create symmetric key')
const nonce = new Uint8Array(24)
crypto.getRandomValues(nonce)
// Encrypt the data with the symmetric key.
@ -420,4 +426,44 @@ export function decryptDeprecatedSingle(uint8Array, publicKey, privateKey) {
throw new Error("Unable to decrypt")
}
return uint8ArrayToBase64(_decryptedData)
}
}
export const decryptGroupEncryptionWithSharingKey = async ({ data64EncryptedData, key }: any) => {
const allCombined = base64ToUint8Array(data64EncryptedData)
const str = "qortalGroupEncryptedData"
const strEncoder = new TextEncoder()
const strUint8Array = strEncoder.encode(str)
// Extract the nonce
const nonceStartPosition = strUint8Array.length
const nonceEndPosition = nonceStartPosition + 24 // Nonce is 24 bytes
const nonce = allCombined.slice(nonceStartPosition, nonceEndPosition)
// Extract the shared keyNonce
const keyNonceStartPosition = nonceEndPosition
const keyNonceEndPosition = keyNonceStartPosition + 24 // Nonce is 24 bytes
const keyNonce = allCombined.slice(keyNonceStartPosition, keyNonceEndPosition)
// Extract the sender's public key
const senderPublicKeyStartPosition = keyNonceEndPosition
const senderPublicKeyEndPosition = senderPublicKeyStartPosition + 32 // Public keys are 32 bytes
// Calculate count first
const countStartPosition = allCombined.length - 4 // 4 bytes before the end, since count is stored in Uint32 (4 bytes)
const countArray = allCombined.slice(countStartPosition, countStartPosition + 4)
const count = new Uint32Array(countArray.buffer)[0]
// Then use count to calculate encryptedData
const encryptedDataStartPosition = senderPublicKeyEndPosition // start position of encryptedData
const encryptedDataEndPosition = allCombined.length - ((count * (32 + 16)) + 4)
const encryptedData = allCombined.slice(encryptedDataStartPosition, encryptedDataEndPosition)
const symmetricKey = base64ToUint8Array(key);
// Decrypt the data using the nonce and messageKey
const decryptedData = nacl.secretbox.open(encryptedData, nonce, symmetricKey)
// Check if decryption was successful
if (!decryptedData) {
throw new Error("Decryption failed");
}
// Convert the decrypted Uint8Array back to a Base64 string
return uint8ArrayToBase64(decryptedData);
};

View File

@ -1,5 +1,5 @@
import { gateways, getApiKeyFromStorage } from "./background";
import { addForeignServer, addListItems, adminAction, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createPoll, decryptData, deleteListItems, deployAt, encryptData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, openNewTab, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get";
import { addForeignServer, addListItems, adminAction, cancelSellOrder, createAndCopyEmbedLink, createBuyOrder, createPoll, decryptData, decryptDataWithSharingKey, decryptQortalGroupData, deleteListItems, deployAt, encryptData, encryptDataWithSharingKey, encryptQortalGroupData, getCrossChainServerInfo, getDaySummary, getForeignFee, getListItems, getServerConnectionHistory, getTxActivitySummary, getUserAccount, getUserWallet, getUserWalletInfo, getWalletBalance, joinGroup, openNewTab, publishMultipleQDNResources, publishQDNResource, removeForeignServer, saveFile, sendChatMessage, sendCoin, setCurrentForeignServer, updateForeignFee, voteOnPoll } from "./qortalRequests/get";
import { getData, storeData } from "./utils/chromeStorage";
@ -713,7 +713,84 @@ export const isRunningGateway = async ()=> {
}
break;
}
case "ENCRYPT_QORTAL_GROUP_DATA": {
try {
const res = await encryptQortalGroupData(request.payload, event.source);
event.source.postMessage({
requestId: request.requestId,
action: request.action,
payload: res,
type: "backgroundMessageResponse",
}, event.origin);
} catch (error) {
event.source.postMessage({
requestId: request.requestId,
action: request.action,
error: error?.message,
type: "backgroundMessageResponse",
}, event.origin);
}
break;
}
case "DECRYPT_QORTAL_GROUP_DATA": {
try {
const res = await decryptQortalGroupData(request.payload, event.source);
event.source.postMessage({
requestId: request.requestId,
action: request.action,
payload: res,
type: "backgroundMessageResponse",
}, event.origin);
} catch (error) {
event.source.postMessage({
requestId: request.requestId,
action: request.action,
error: error?.message,
type: "backgroundMessageResponse",
}, event.origin);
}
break;
}
case "ENCRYPT_DATA_WITH_SHARING_KEY": {
try {
const res = await encryptDataWithSharingKey(request.payload, isFromExtension)
event.source.postMessage({
requestId: request.requestId,
action: request.action,
payload: res,
type: "backgroundMessageResponse",
}, event.origin);
} catch (error) {
event.source.postMessage({
requestId: request.requestId,
action: request.action,
error: error?.message,
type: "backgroundMessageResponse",
}, event.origin);
}
break;
}
case "DECRYPT_DATA_WITH_SHARING_KEY": {
try {
const res = await decryptDataWithSharingKey(request.payload, isFromExtension)
event.source.postMessage({
requestId: request.requestId,
action: request.action,
payload: res,
type: "backgroundMessageResponse",
}, event.origin);
} catch (error) {
event.source.postMessage({
requestId: request.requestId,
action: request.action,
error: error?.message,
type: "backgroundMessageResponse",
}, event.origin);
}
break;
}
default:
break;
}

View File

@ -14,17 +14,23 @@ import {
sendCoin as sendCoinFunc,
isUsingLocal,
createBuyOrderTx,
performPowTask
performPowTask,
groupSecretkeys
} from "../background";
import { getNameInfo } from "../backgroundFunctions/encryption";
import { getNameInfo, uint8ArrayToObject } from "../backgroundFunctions/encryption";
import { showSaveFilePicker } from "../components/Apps/useQortalMessageListener";
import { QORT_DECIMALS } from "../constants/constants";
import Base58 from "../deps/Base58";
import {
base64ToUint8Array,
createSymmetricKeyAndNonce,
decryptDeprecatedSingle,
decryptGroupDataQortalRequest,
decryptGroupEncryptionWithSharingKey,
decryptSingle,
encryptDataGroup,
encryptSingle,
objectToBase64,
uint8ArrayStartsWith,
uint8ArrayToBase64,
} from "../qdn/encryption/group-encryption";
@ -37,6 +43,7 @@ import DeleteTradeOffer from "../transactions/TradeBotDeleteRequest";
import signTradeBotTransaction from "../transactions/signTradeBotTransaction";
import { executeEvent } from "../utils/events";
import { extractComponents } from "../components/Chat/MessageDisplay";
import { decryptResource, getGroupAdmins, getPublishesFromAdmins, validateSecretKey } from "../components/Group/Group";
const btcFeePerByte = 0.00000100
const ltcFeePerByte = 0.00000030
@ -374,6 +381,168 @@ export const encryptData = async (data, sender) => {
throw new Error("Unable to encrypt");
}
};
export const encryptQortalGroupData = async (data, sender) => {
let data64 = data.data64;
let groupId = data?.groupId
if(!groupId){
throw new Error('Please provide a groupId')
}
if (data.fileId) {
data64 = await getFileFromContentScript(data.fileId);
}
if (!data64) {
throw new Error("Please include data to encrypt");
}
let secretKeyObject
if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){
secretKeyObject = groupSecretkeys[groupId].secretKeyObject
}
if(!secretKeyObject){
const { names } =
await getGroupAdmins(groupId)
const publish =
await getPublishesFromAdmins(names, groupId);
if(publish === false) throw new Error('No group key found.')
const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
publish.identifier
}?encoding=base64`);
const res = await fetch(
url
);
const resData = await res.text();
const decryptedKey: any = await decryptResource(resData);
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid");
secretKeyObject = decryptedKeyToObject
groupSecretkeys[groupId] = {
secretKeyObject,
timestamp: Date.now()
}
}
const resGroupEncryptedResource = encryptSingle({
data64, secretKeyObject: secretKeyObject,
})
if (resGroupEncryptedResource) {
return resGroupEncryptedResource;
} else {
throw new Error("Unable to encrypt");
}
};
export const decryptQortalGroupData = async (data, sender) => {
let data64 = data.data64;
let groupId = data?.groupId
if(!groupId){
throw new Error('Please provide a groupId')
}
if (data.fileId) {
data64 = await getFileFromContentScript(data.fileId);
}
if (!data64) {
throw new Error("Please include data to encrypt");
}
let secretKeyObject
if(groupSecretkeys[groupId] && groupSecretkeys[groupId].secretKeyObject && groupSecretkeys[groupId]?.timestamp && (Date.now() - groupSecretkeys[groupId]?.timestamp) < 1200000){
secretKeyObject = groupSecretkeys[groupId].secretKeyObject
}
if(!secretKeyObject){
const { names } =
await getGroupAdmins(groupId)
const publish =
await getPublishesFromAdmins(names, groupId);
if(publish === false) throw new Error('No group key found.')
const url = await createEndpoint(`/arbitrary/DOCUMENT_PRIVATE/${publish.name}/${
publish.identifier
}?encoding=base64`);
const res = await fetch(
url
);
const resData = await res.text();
const decryptedKey: any = await decryptResource(resData);
const dataint8Array = base64ToUint8Array(decryptedKey.data);
const decryptedKeyToObject = uint8ArrayToObject(dataint8Array);
if (!validateSecretKey(decryptedKeyToObject))
throw new Error("SecretKey is not valid");
secretKeyObject = decryptedKeyToObject
groupSecretkeys[groupId] = {
secretKeyObject,
timestamp: Date.now()
}
}
const resGroupDecryptResource = decryptSingle({
data64, secretKeyObject: secretKeyObject, skipDecodeBase64: true
})
if (resGroupDecryptResource) {
return resGroupDecryptResource;
} else {
throw new Error("Unable to decrypt");
}
};
export const encryptDataWithSharingKey = async (data, sender) => {
let data64 = data.data64;
let publicKeys = data.publicKeys || [];
if (data.fileId) {
data64 = await getFileFromContentScript(data.fileId);
}
if (!data64) {
throw new Error("Please include data to encrypt");
}
const symmetricKey = createSymmetricKeyAndNonce()
const dataObject = {
data: data64,
key:symmetricKey.messageKey
}
const dataObjectBase64 = await objectToBase64(dataObject)
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const privateKey = parsedData.privateKey;
const userPublicKey = parsedData.publicKey;
const encryptDataResponse = encryptDataGroup({
data64: dataObjectBase64,
publicKeys: publicKeys,
privateKey,
userPublicKey,
customSymmetricKey: symmetricKey.messageKey
});
if (encryptDataResponse) {
return encryptDataResponse;
} else {
throw new Error("Unable to encrypt");
}
};
export const decryptDataWithSharingKey = async (data, sender) => {
const { encryptedData, key } = data;
if (!encryptedData) {
throw new Error("Please include data to decrypt");
}
const decryptedData = await decryptGroupEncryptionWithSharingKey({data64EncryptedData: encryptedData, key})
const base64ToObject = JSON.parse(atob(decryptedData))
if(!base64ToObject.data) throw new Error('No data in the encrypted resource')
return base64ToObject.data
};
export const decryptData = async (data) => {
const { encryptedData, publicKey } = data;
@ -579,7 +748,11 @@ export const deleteListItems = async (data, isFromExtension) => {
}
};
export const publishQDNResource = async (data: any, sender, isFromExtension) => {
export const publishQDNResource = async (
data: any,
sender,
isFromExtension
) => {
const requiredFields = ["service"];
const missingFields: string[] = [];
requiredFields.forEach((field) => {
@ -613,6 +786,9 @@ export const publishQDNResource = async (data: any, sender, isFromExtension) =>
if (data.identifier == null) {
identifier = "default";
}
if (data.fileId) {
data64 = await getFileFromContentScript(data.fileId);
}
if (
data.encrypt &&
(!data.publicKeys ||
@ -620,23 +796,19 @@ export const publishQDNResource = async (data: any, sender, isFromExtension) =>
) {
throw new Error("Encrypting data requires public keys");
}
if (!data.encrypt && data.service.endsWith("_PRIVATE")) {
throw new Error("Only encrypted data can go into private services");
}
if (data.fileId) {
data64 = await getFileFromContentScript(data.fileId);
}
if (data.encrypt) {
try {
const resKeyPair = await getKeyPair()
const parsedData = resKeyPair
const privateKey = parsedData.privateKey
const userPublicKey = parsedData.publicKey
const resKeyPair = await getKeyPair();
const parsedData = resKeyPair;
const privateKey = parsedData.privateKey;
const userPublicKey = parsedData.publicKey;
const encryptDataResponse = encryptDataGroup({
data64,
publicKeys: data.publicKeys,
privateKey,
userPublicKey
userPublicKey,
});
if (encryptDataResponse) {
data64 = encryptDataResponse;
@ -650,13 +822,16 @@ export const publishQDNResource = async (data: any, sender, isFromExtension) =>
const fee = await getFee("ARBITRARY");
const resPermission = await getUserPermission({
text1: "Do you give this application permission to publish to QDN?",
text2: `service: ${service}`,
text3: `identifier: ${identifier || null}`,
highlightedText: `isEncrypted: ${!!data.encrypt}`,
fee: fee.fee,
}, isFromExtension);
const resPermission = await getUserPermission(
{
text1: "Do you give this application permission to publish to QDN?",
text2: `service: ${service}`,
text3: `identifier: ${identifier || null}`,
highlightedText: data?.externalEncrypt ? `App is externally encrypting the resource. Make sure you trust the app.` : `isEncrypted: ${!!data.encrypt}`,
fee: fee.fee,
},
isFromExtension
);
const { accepted } = resPermission;
if (accepted) {
@ -689,6 +864,7 @@ export const publishQDNResource = async (data: any, sender, isFromExtension) =>
}
};
export const publishMultipleQDNResources = async (data: any, sender, isFromExtension) => {
const requiredFields = ["resources"];
const missingFields: string[] = [];
@ -2897,7 +3073,17 @@ const missingFieldsFunc = (data, requiredFields)=> {
}
const encode = (value) => encodeURIComponent(value.trim()); // Helper to encode values
const buildQueryParams = (data) => {
const allowedParams= ["name", "service", "identifier", "mimeType", "fileName", "encryptionType", "key"]
return Object.entries(data)
.map(([key, value]) => {
if (value === undefined || value === null || value === false || !allowedParams.includes(key)) return null; // Skip null, undefined, or false
if (typeof value === "boolean") return `${key}=${value}`; // Handle boolean values
return `${key}=${encode(value)}`; // Encode other values
})
.filter(Boolean) // Remove null values
.join("&"); // Join with `&`
};
export const createAndCopyEmbedLink = async (data, isFromExtension) => {
const requiredFields = [
"type",
@ -2929,53 +3115,33 @@ export const createAndCopyEmbedLink = async (data, isFromExtension) => {
.filter(Boolean) // Remove null values
.join("&"); // Join with `&`
const link = `qortal://use-embed/POLL?${queryParams}`
navigator.clipboard.writeText(link)
.then(() => {
executeEvent('openGlobalSnackBar', {
message: 'Copied link to clipboard',
type: 'info'
})
//success
})
.catch((error) => {
executeEvent('openGlobalSnackBar', {
message: 'Failed to copy to clipboard',
type: 'error'
})
// error
});
try {
await navigator.clipboard.writeText(link);
} catch (error) {
throw new Error('Failed to copy to clipboard.')
}
return link;
}
case "IMAGE": {
case "IMAGE":
case "ATTACHMENT":
{
missingFieldsFunc(data, [
"type",
"name",
"service",
"identifier"
])
const queryParams = [
`name=${encode(data.name)}`,
`service=${encode(data.service)}`,
`identifier=${encode(data.identifier)}`,
data.ref ? `ref=${encode(data.ref)}` : null, // Add only if ref exists
]
.filter(Boolean) // Remove null values
.join("&"); // Join with `&`
if(data?.encryptionType === 'private' && !data?.key){
throw new Error('For an encrypted resource, you must provide the key to create the shared link')
}
const queryParams = buildQueryParams(data)
const link = `qortal://use-embed/IMAGE?${queryParams}`;
const link = `qortal://use-embed/${data.type}?${queryParams}`;
try {
await navigator.clipboard.writeText(link);
executeEvent("openGlobalSnackBar", {
message: "Copied link to clipboard",
type: "info",
});
} catch (error) {
executeEvent("openGlobalSnackBar", {
message: "Failed to copy to clipboard",
type: "error",
});
throw new Error('Failed to copy to clipboard.')
}
return link;

View File

@ -54,4 +54,14 @@ export const fileToBase64 = (file) => new Promise(async (resolve, reject) => {
reject(error)
semaphore.release()
}
})
})
export const base64ToBlobUrl = (base64, mimeType = "image/png") => {
const binary = atob(base64);
const array = [];
for (let i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
const blob = new Blob([new Uint8Array(array)], { type: mimeType });
return URL.createObjectURL(blob);
};

View File

@ -1,7 +1,9 @@
// @ts-nocheck
import { saveFileInChunks, showSaveFilePicker } from '../../components/Apps/useQortalMessageListener';
import { crypto, walletVersion } from '../../constants/decryptWallet';
import { doInitWorkers, kdf } from '../../deps/kdf';
import { mimeToExtensionMap } from '../memeTypes';
import PhraseWallet from './phrase-wallet';
import * as WORDLISTS from './wordlists';
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
@ -109,4 +111,21 @@ export const saveSeedPhraseToDisk = async (data) => {
encoding: Encoding.UTF8,
});
}
const hasExtension = (filename) => {
return filename.includes(".") && filename.split(".").pop().length > 0;
};
export const saveFileToDiskGeneric = async (blob, filename) => {
const timestamp = new Date()
.toISOString()
.replace(/:/g, "-"); // Safe timestamp for filenames
const fileExtension = mimeToExtensionMap[blob.type]
let fileName = filename || "qortal_file_" + timestamp + "." + fileExtension;
fileName = hasExtension(fileName) ? fileName : fileName + "." + fileExtension;
await saveFileInChunks(blob, fileName)
// await FileSaver.saveAs(blob, fileName);
}