3
0
mirror of https://github.com/Qortal/q-mail.git synced 2025-02-11 17:55:56 +00:00

fix attachment download, more info when downloading a message, using new multi-publish to create message

This commit is contained in:
PhilReact 2023-12-30 15:41:19 +02:00
parent 3e71890f7b
commit 3dbb791eba
22 changed files with 2232 additions and 3704 deletions

4045
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@
"dompurify": "^3.0.3",
"flexlayout-react": "^0.7.9",
"localforage": "^1.10.0",
"mime": "^4.0.1",
"moment": "^2.29.4",
"philliplm-react-modern-audio-player": "^1.4.6",
"react": "^18.2.0",
@ -50,12 +51,11 @@
"@types/dompurify": "^3.0.2",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-legacy": "^4.0.3",
"@vitejs/plugin-react-swc": "^3.2.0",
"@vitejs/plugin-react": "^4.2.1",
"core-js": "^3.30.2",
"prettier": "^2.8.6",
"typescript": "^4.9.3",
"vite": "^4.2.0",
"vite": "^5.0.10",
"worker-loader": "^3.0.8"
}
}

View File

@ -0,0 +1,23 @@
import { IconTypes } from "./IconTypes";
export const CircleSVG: React.FC<IconTypes> = ({
color,
height,
width,
className,
onClickFunc,
}) => {
return (
<svg
onClick={onClickFunc}
className={className}
fill={color}
xmlns="http://www.w3.org/2000/svg"
height={height}
viewBox="0 -960 960 960"
width={width}
>
<path d="m424-296 282-282-56-56-226 226-114-114-56 56 170 170Zm56 216q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" />
</svg>
);
};

View File

@ -0,0 +1,23 @@
import { IconTypes } from "./IconTypes";
export const EmptyCircleSVG: React.FC<IconTypes> = ({
color,
height,
width,
className,
onClickFunc,
}) => {
return (
<svg onClick={onClickFunc}
className={className}
fill={color}
height={height}
width={width} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" ><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
);
};

View File

@ -1,342 +1,313 @@
import * as React from 'react'
import { styled, useTheme } from '@mui/material/styles'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import AudiotrackIcon from '@mui/icons-material/Audiotrack'
import { MyContext } from '../wrappers/DownloadWrapper'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../state/store'
import { CircularProgress } from '@mui/material'
import AttachFileIcon from '@mui/icons-material/AttachFile'
import {
setCurrAudio,
setShowingAudioPlayer
} from '../state/features/globalSlice'
import {
base64ToUint8Array,
objectToUint8ArrayFromResponse
} from '../utils/toBase64'
import { setNotification } from '../state/features/notificationsSlice'
import * as React from "react";
import { styled, useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useDispatch, useSelector } from "react-redux";
import { CircularProgress } from "@mui/material";
import { MyContext } from "../wrappers/DownloadWrapper";
import { RootState } from "../state/store";
import { setNotification } from "../state/features/notificationsSlice";
import { base64ToUint8Array } from "../utils/toBase64";
const Widget = styled('div')(({ theme }) => ({
const Widget = styled("div")(({ theme }) => ({
padding: 8,
borderRadius: 10,
maxWidth: 350,
position: 'relative',
position: "relative",
zIndex: 1,
// backgroundColor:
// theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.4)',
backdropFilter: 'blur(40px)',
background: 'skyblue',
transition: '0.2s all',
'&:hover': {
opacity: 0.75
}
}))
backdropFilter: "blur(40px)",
background: "skyblue",
transition: "0.2s all",
"&:hover": {
opacity: 0.75,
},
}));
const CoverImage = styled('div')({
const CoverImage = styled("div")({
width: 40,
height: 40,
objectFit: 'cover',
overflow: 'hidden',
objectFit: "cover",
overflow: "hidden",
flexShrink: 0,
borderRadius: 8,
backgroundColor: 'rgba(0,0,0,0.08)',
'& > img': {
width: '100%'
}
})
const TinyText = styled(Typography)({
fontSize: '0.75rem',
opacity: 0.38,
fontWeight: 500,
letterSpacing: 0.2
})
backgroundColor: "rgba(0,0,0,0.08)",
"& > img": {
width: "100%",
},
});
interface IAudioElement {
title: string
description?: string
author?: string
fileInfo?: any
postId?: string
user?: string
children?: React.ReactNode
mimeType?: string
disable?: boolean
mode?: string
otherUser?: string
title: string;
description?: string;
author?: string;
fileInfo?: any;
postId?: string;
user?: string;
children?: React.ReactNode;
mimeTypeSaved?: string;
disable?: boolean;
mode?: string;
otherUser?: string;
customStyles?: any;
}
interface CustomWindow extends Window {
showSaveFilePicker: any // Replace 'any' with the appropriate type if you know it
showSaveFilePicker: any; // Replace 'any' with the appropriate type if you know it
}
const customWindow = window as unknown as CustomWindow
const customWindow = window as unknown as CustomWindow;
export default function FileElement({
title,
description,
author,
fileInfo,
postId = '',
user,
children,
mimeType,
mimeTypeSaved,
disable,
mode,
otherUser
customStyles,
}: IAudioElement) {
const { downloadVideo } = React.useContext(MyContext)
const [isLoading, setIsLoading] = React.useState<boolean>(false)
const [fileProperties, setFileProperties] = React.useState<any>(null)
const [downloadLoader, setDownloadLoader] = React.useState<any>(false)
const { downloadVideo } = React.useContext(MyContext);
const [startedDownload, setStartedDownload] = React.useState<boolean>(false)
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [downloadLoader, setDownloadLoader] = React.useState<any>(false);
const downloads = useSelector((state: RootState) => state.global?.downloads);
const hasCommencedDownload = React.useRef(false);
const dispatch = useDispatch();
const reDownload = React.useRef<boolean>(false)
const status = React.useRef<null | string>(null)
const [pdfSrc, setPdfSrc] = React.useState('')
const { downloads } = useSelector((state: RootState) => state.global)
const { user: username } = useSelector((state: RootState) => state.auth)
const hasCommencedDownload = React.useRef(false)
const dispatch = useDispatch()
const isFetchingProperties = React.useRef<boolean>(false)
const download = React.useMemo(() => {
if (!downloads || !fileInfo?.identifier) return {}
const findDownload = downloads[fileInfo?.identifier]
if (!downloads || !fileInfo?.identifier) return {};
const findDownload = downloads[fileInfo?.identifier];
if (!findDownload) return {}
return findDownload
}, [downloads, fileInfo])
if (!findDownload) return {};
return findDownload;
}, [downloads, fileInfo]);
const resourceStatus = React.useMemo(() => {
return download?.status || {}
}, [download])
const saveFileToDisk = async (blob: any, fileName: any) => {
try {
const fileHandle = await customWindow.showSaveFilePicker({
suggestedName: fileName,
types: [
{
description: 'File'
}
]
})
const writeFile = async (fileHandle: any, contents: any) => {
const writable = await fileHandle.createWritable()
await writable.write(contents)
await writable.close()
}
writeFile(fileHandle, blob).then(() => console.log('FILE SAVED'))
} catch (error) {
console.log(error)
}
}
return download?.status || {};
}, [download]);
console.log({download})
const handlePlay = async () => {
if (disable) return
hasCommencedDownload.current = true
if (disable) return;
hasCommencedDownload.current = true;
setStartedDownload(true)
if (
resourceStatus?.status === 'READY' &&
download?.url &&
download?.blogPost?.filename
resourceStatus?.status === "READY"
) {
if (downloadLoader) return
dispatch(
setNotification({
msg: 'Saving file... please wait',
alertType: 'info'
})
)
setDownloadLoader(true)
if (downloadLoader) return;
setDownloadLoader(true);
let filename = download?.properties?.filename
let mimeType = download?.properties?.type
try {
const { name, service, identifier } = fileInfo
if (mode === 'mail') {
let res = await qortalRequest({
const { name, service, identifier } = fileInfo;
const res = await qortalRequest({
action: "GET_QDN_RESOURCE_PROPERTIES",
name: name,
service: service,
identifier: identifier,
});
filename = res?.filename || filename;
mimeType = res?.mimeType || mimeType || mimeTypeSaved;
} catch (error) {
}
try {
const { name, service, identifier } = fileInfo;
let resData = await qortalRequest({
action: 'FETCH_QDN_RESOURCE',
name: name,
service: service,
identifier: identifier,
encoding: 'base64'
})
// const toUnit8Array = base64ToUint8Array(res)
const resName = await qortalRequest({
action: 'GET_NAME_DATA',
// change this
name: otherUser
})
if (!resName?.owner)
throw new Error('Unable to locate details to decrypt file')
const recipientAddress = resName.owner
const resAddress = await qortalRequest({
action: 'GET_ACCOUNT_DATA',
address: recipientAddress
})
if (!resAddress?.publicKey)
throw new Error('Unable to locate details to decrypt file')
const recipientPublicKey = resAddress.publicKey
let requestEncryptBody: any = {
action: 'DECRYPT_DATA',
encryptedData: res,
publicKey: recipientPublicKey
}
encryptedData: resData }
const resDecrypt = await qortalRequest(requestEncryptBody)
if (!resDecrypt) throw new Error('Unable to decrypt file')
const decryptToUnit8Array = base64ToUint8Array(resDecrypt)
let blob = null
if (download?.blogPost?.mimeType) {
if (mimeType) {
blob = new Blob([decryptToUnit8Array], {
type: download?.blogPost?.mimeType
type: mimeType
})
} else {
blob = new Blob([decryptToUnit8Array])
}
if (!blob) throw new Error('Unable build file into blob')
if (!blob) throw new Error('Unable to build file into blob')
await qortalRequest({
action: 'SAVE_FILE',
blob,
filename:
download?.blogPost?.originalFilename ||
download?.blogPost?.filename,
mimeType: download?.blogPost?.mimeType || ''
download?.properties?.originalFilename ||
filename,
mimeType
})
return
}
const url = `/arbitrary/${service}/${name}/${identifier}`
fetch(url)
.then((response) => response.blob())
.then(async (blob) => {
await qortalRequest({
action: 'SAVE_FILE',
blob,
filename: download?.blogPost?.filename,
mimeType: download?.blogPost?.mimeType || ''
})
// saveAs(blob, download?.blogPost?.filename)
})
.catch((error) => {
console.error('Error fetching the video:', error)
// clearInterval(intervalId)
})
//old
// const url = `/arbitrary/${service}/${name}/${identifier}`;
// fetch(url)
// .then(response => response.blob())
// .then(async blob => {
// await qortalRequest({
// action: "SAVE_FILE",
// blob,
// filename: filename,
// mimeType,
// });
// })
// .catch(error => {
// console.error("Error fetching the video:", error);
// });
} catch (error: any) {
let notificationObj = null
if (typeof error === 'string') {
let notificationObj: any = null;
if (typeof error === "string") {
notificationObj = {
msg: error || 'Failed to send message',
alertType: 'error'
}
} else if (typeof error?.error === 'string') {
msg: error || "Failed to send message",
alertType: "error",
};
} else if (typeof error?.error === "string") {
notificationObj = {
msg: error?.error || 'Failed to send message',
alertType: 'error'
}
msg: error?.error || "Failed to send message",
alertType: "error",
};
} else {
notificationObj = {
msg: error?.message || 'Failed to send message',
alertType: 'error'
}
msg: error?.message || "Failed to send message",
alertType: "error",
};
}
if (!notificationObj) return
dispatch(setNotification(notificationObj))
if (!notificationObj) return;
dispatch(setNotification(notificationObj));
} finally {
setDownloadLoader(false)
setDownloadLoader(false);
}
return
return;
}
if (!postId && mode !== 'mail') return
const { name, service, identifier } = fileInfo
let filename = fileProperties?.filename
let mimeType = fileProperties?.mimeType
if (!fileProperties) {
try {
dispatch(
setNotification({
msg: 'Downloading file... please wait',
alertType: 'info'
})
)
let res = await qortalRequest({
action: 'GET_QDN_RESOURCE_PROPERTIES',
name: name,
service: service,
identifier: identifier
})
setFileProperties(res)
filename = res?.filename
mimeType = res?.mimeType
} catch (error: any) {
console.log({ error })
dispatch(
setNotification({
msg: error?.message || 'Error with download. Please try again',
alertType: 'error'
})
)
}
}
if (!filename) return
const { name, service, identifier } = fileInfo;
setIsLoading(true);
downloadVideo({
name,
service,
identifier,
blogPost: {
postId,
user,
audioTitle: title,
audioDescription: description,
audioAuthor: author,
filename,
mimeType,
originalFilename: fileInfo?.originalFilename
}
})
properties: {
...fileInfo,
},
});
};
const refetch = React.useCallback(async () => {
if (!fileInfo) return
try {
const { name, service, identifier } = fileInfo;
isFetchingProperties.current = true
await qortalRequest({
action: 'GET_QDN_RESOURCE_PROPERTIES',
name,
service,
identifier
})
} catch (error) {
} finally {
isFetchingProperties.current = false
}
}, [fileInfo])
const refetchInInterval = ()=> {
try {
const interval = setInterval(()=> {
if(status?.current === 'DOWNLOADED'){
refetch()
}
if(status?.current === 'READY'){
clearInterval(interval);
}
}, 7500)
} catch (error) {
}
}
React.useEffect(() => {
if(resourceStatus?.status){
status.current = resourceStatus?.status
}
if (
resourceStatus?.status === 'READY' &&
resourceStatus?.status === "READY" &&
download?.url &&
download?.blogPost?.filename &&
download?.properties?.filename &&
hasCommencedDownload.current
) {
setIsLoading(false)
setIsLoading(false);
dispatch(
setNotification({
msg: 'Download completed. Click to save file',
alertType: 'info'
msg: "Download completed. Click to save file",
alertType: "info",
})
)
);
} else if (
resourceStatus?.status === 'DOWNLOADED' &&
reDownload?.current === false
) {
refetchInInterval()
reDownload.current = true
}
}, [resourceStatus, download])
}, [resourceStatus, download]);
return (
<Box
onClick={handlePlay}
sx={{
width: '100%',
overflow: 'hidden',
position: 'relative',
cursor: 'pointer'
width: "100%",
overflow: "hidden",
position: "relative",
cursor: "pointer",
...(customStyles || {}),
}}
>
{children && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
position: 'relative',
gap: '7px'
display: "flex",
alignItems: "center",
position: "relative",
gap: "7px",
}}
>
{children}{' '}
{(resourceStatus.status && resourceStatus?.status !== 'READY') ||
isLoading ? (
<CircularProgress color="secondary" size={14} />
) : resourceStatus?.status === 'READY' ? (
{children}{" "}
{((resourceStatus.status && resourceStatus?.status !== "READY") ||
isLoading) && startedDownload ? (
<>
<CircularProgress color="secondary" size={14} />
<Typography variant="body2">{`${Math.round(
resourceStatus?.percentLoaded || 0
).toFixed(0)}% loaded`}</Typography>
</>
) : resourceStatus?.status === "READY" ? (
<>
<Typography
sx={{
fontSize: '14px'
fontSize: "14px",
}}
>
Ready to save: click here
@ -348,156 +319,6 @@ export default function FileElement({
) : null}
</Box>
)}
{!children && (
<Widget>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CoverImage>
<AttachFileIcon
sx={{
width: '90%',
height: 'auto'
}}
/>
</CoverImage>
<Box sx={{ ml: 1.5, minWidth: 0 }}>
<Typography
variant="caption"
color="text.secondary"
fontWeight={500}
>
{author}
</Typography>
<Typography
noWrap
sx={{
fontSize: '16px'
}}
>
<b>{title}</b>
</Typography>
<Typography
noWrap
letterSpacing={-0.25}
sx={{
fontSize: '14px'
}}
>
{description}
</Typography>
{mimeType && (
<Typography
noWrap
letterSpacing={-0.25}
sx={{
fontSize: '12px'
}}
>
{mimeType}
</Typography>
)}
</Box>
</Box>
{((resourceStatus.status && resourceStatus?.status !== 'READY') ||
isLoading) && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
display="flex"
justifyContent="center"
alignItems="center"
zIndex={4999}
bgcolor="rgba(0, 0, 0, 0.6)"
sx={{
display: 'flex',
flexDirection: 'column',
gap: '10px',
padding: '8px',
borderRadius: '10px'
}}
>
<CircularProgress color="secondary" />
{resourceStatus && (
<Typography
variant="subtitle2"
component="div"
sx={{
color: 'white',
fontSize: '14px'
}}
>
{resourceStatus?.status === 'REFETCHING' ? (
<>
<>
{(
(resourceStatus?.localChunkCount /
resourceStatus?.totalChunkCount) *
100
)?.toFixed(0)}
%
</>
<> Refetching in 2 minutes</>
</>
) : resourceStatus?.status === 'DOWNLOADED' ? (
<>Download Completed: building file...</>
) : resourceStatus?.status !== 'READY' ? (
<>
{(
(resourceStatus?.localChunkCount /
resourceStatus?.totalChunkCount) *
100
)?.toFixed(0)}
%
</>
) : (
<>Download Completed: fetching file...</>
)}
</Typography>
)}
</Box>
)}
{resourceStatus?.status === 'READY' &&
download?.url &&
download?.blogPost?.filename && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
display="flex"
justifyContent="center"
alignItems="center"
zIndex={4999}
bgcolor="rgba(0, 0, 0, 0.6)"
sx={{
display: 'flex',
flexDirection: 'row',
gap: '10px',
padding: '8px',
borderRadius: '10px'
}}
>
<Typography
variant="subtitle2"
component="div"
sx={{
color: 'white',
fontSize: '14px'
}}
>
Ready to save: click here
</Typography>
{downloadLoader && (
<CircularProgress color="secondary" size={14} />
)}
</Box>
)}
</Widget>
)}
</Box>
)
);
}

View File

@ -0,0 +1,201 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Box,
Button,
CircularProgress,
Modal,
Typography,
useTheme,
} from "@mui/material";
import React, { useCallback, useEffect, useState, useRef } from "react";
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
import { styled } from "@mui/system";
interface Publish {
resources: any[];
action: string;
}
interface MultiplePublishProps {
publishes: Publish;
isOpen: boolean;
onSubmit: ()=> void
}
export const MultiplePublish = ({ publishes, isOpen, onSubmit}: MultiplePublishProps) => {
const theme = useTheme();
const listOfSuccessfulPublishesRef = useRef([])
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
any[]
>([]);
const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] = useState<
any[]
>([]);
const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
const hasStarted = useRef(false);
const publish = useCallback(async (pub: any) => {
const lengthOfResources = pub?.resources?.length
const lengthOfTimeout = lengthOfResources * 30000
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
}, []);
const [isPublishing, setIsPublishing] = useState(true)
const handlePublish = useCallback(
async (pub: any) => {
try {
setCurrentlyInPublish(pub?.identifier);
setIsPublishing(true)
const res = await publish(pub);
onSubmit()
setListOfUnSuccessfulPublishes([])
} catch (error: any) {
const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || []
console.log({ error });
if(unsuccessfulPublishes?.length > 0){
setListOfUnSuccessfulPublishes(unsuccessfulPublishes)
}
} finally {
setIsPublishing(false)
}
},
[publish]
);
const retry = ()=> {
let newlistOfMultiplePublishes: any[] = [];
listOfUnsuccessfulPublishes?.forEach((item)=> {
const findPub = publishes?.resources.find((res: any)=> res?.identifier === item.identifier)
if(findPub){
newlistOfMultiplePublishes.push(findPub)
}
})
const multiplePublish = {
...publishes,
resources: newlistOfMultiplePublishes
};
handlePublish(multiplePublish)
}
const startPublish = useCallback(
async (pubs: any) => {
await handlePublish(pubs);
},
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
);
useEffect(() => {
if (publishes && !hasStarted.current) {
hasStarted.current = true;
startPublish(publishes);
}
}, [startPublish, publishes, listOfSuccessfulPublishes]);
return (
<Modal
open={isOpen}
aria-labelledby="modal-title"
aria-describedby="modal-description"
>
<ModalBody
sx={{
minHeight: "50vh",
}}
>
{publishes?.resources?.map((publish: any) => {
const unpublished = listOfUnsuccessfulPublishes.map(item => item?.identifier)
return (
<Box
sx={{
display: "flex",
gap: "20px",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography>{publish?.identifier}</Typography>
{!isPublishing && hasStarted.current ? (
<>
{!unpublished.includes(publish.identifier) ? (
<CircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
) : (
<EmptyCircleSVG
color={theme.palette.text.primary}
height="24px"
width="24px"
/>
)}
</>
): <CircularProgress size={16} color="secondary"/>}
</Box>
);
})}
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (
<>
<Typography sx={{
marginTop: '20px',
fontSize: '16px'
}}>Some files were not published. Please try again. It's important that all the files get published. Maybe wait a couple minutes if the error keeps occurring</Typography>
<Button variant="contained" onClick={()=> {
retry()
}}>Try again</Button>
</>
)}
</ModalBody>
</Modal>
);
};
export const ModalBody = styled(Box)(({ theme }) => ({
position: "absolute",
backgroundColor: theme.palette.background.default,
borderRadius: "4px",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "75%",
maxWidth: "900px",
padding: "15px 35px",
display: "flex",
flexDirection: "column",
gap: "17px",
overflowY: "auto",
maxHeight: "95vh",
boxShadow:
theme.palette.mode === "dark"
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
"&::-webkit-scrollbar-track": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar-track:hover": {
backgroundColor: theme.palette.background.paper,
},
"&::-webkit-scrollbar": {
width: "16px",
height: "10px",
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
},
"&::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
},
"&::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
},
}));

View File

@ -0,0 +1,46 @@
import { useRef, useState } from 'react';
interface State {
isShow: boolean;
}
export const useModal = () => {
const [state, setState] = useState<State>({
isShow: false,
});
const promiseConfig = useRef<any>(null);
const show = async () => {
return new Promise((resolve, reject) => {
promiseConfig.current = {
resolve,
reject,
};
setState({
isShow: true,
});
});
};
const hide = () => {
setState({
isShow: false,
});
};
const onOk = (payload:any) => {
const { resolve } = promiseConfig.current;
hide();
resolve(payload);
};
const onCancel = () => {
const { reject } = promiseConfig.current;
hide();
reject();
};
return {
show,
onOk,
onCancel,
isShow: state.isShow
};
};

View File

@ -262,10 +262,7 @@ export const useFetchMail = () => {
const offset = mailMessages.length
dispatch(setIsLoadingGlobal(true))
const query = `qortal_qmail_${recipientName.slice(
0,
20
)}_${recipientAddress.slice(-6)}_mail_`
const query = `qortal_qmail_`
const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true`
const response = await fetch(url, {
method: 'GET',

View File

@ -19,7 +19,6 @@ import Tabs from '@mui/material/Tabs'
import Tab from '@mui/material/Tab'
import { useFetchMail } from '../../hooks/useFetchMail'
import { ShowMessage } from './ShowMessage'
import { fetchAndEvaluateMail } from '../../utils/fetchMail'
import { addToHashMapMail } from '../../state/features/mailSlice'
import {
setIsLoadingGlobal,
@ -28,11 +27,15 @@ import {
import SimpleTable from './MailTable'
import { MAIL_SERVICE_TYPE } from '../../constants/mail'
import { BlogPost } from '../../state/features/blogSlice'
import { useModal } from '../../components/common/useModal'
import { OpenMail } from './OpenMail'
interface AliasMailProps {
value: string
}
export const AliasMail = ({ value }: AliasMailProps) => {
const {isShow, onCancel, onOk, show} = useModal()
const theme = useTheme()
const { user } = useSelector((state: RootState) => state.auth)
const [isOpen, setIsOpen] = useState<boolean>(false)
@ -41,6 +44,7 @@ export const AliasMail = ({ value }: AliasMailProps) => {
const [valueTab, setValueTab] = React.useState(0)
const [aliasValue, setAliasValue] = useState('')
const [alias, setAlias] = useState<string[]>([])
const [mailInfo, setMailInfo] = useState<any>(null)
const hashMapPosts = useSelector(
(state: RootState) => state.blog.hashMapPosts
)
@ -232,25 +236,27 @@ export const AliasMail = ({ value }: AliasMailProps) => {
content: any
) => {
try {
const existingMessage = hashMapMailMessages[messageIdentifier]
if (existingMessage) {
const existingMessage: any = hashMapMailMessages[messageIdentifier]
if (existingMessage && existingMessage.isValid && !existingMessage.unableToDecrypt) {
setMessage(existingMessage)
setIsOpen(true)
return
}
dispatch(setIsLoadingGlobal(true))
const res = await fetchAndEvaluateMail({
user,
messageIdentifier,
content,
otherUser: user
setMailInfo({
identifier: messageIdentifier,
name: user,
service: MAIL_SERVICE_TYPE
})
setMessage(res)
dispatch(addToHashMapMail(res))
setIsOpen(true)
const res: any = await show()
setMailInfo(null)
const existingMessageAgain = hashMapMailMessages[messageIdentifier]
if (res && res.isValid && !res.unableToDecrypt) {
setMessage(res)
setIsOpen(true)
return
}
} catch (error) {
} finally {
dispatch(setIsLoadingGlobal(false))
}
}
@ -291,6 +297,9 @@ export const AliasMail = ({ value }: AliasMailProps) => {
<Button onClick={getMessages}>Load Older Messages</Button>
)}
</Box>
{mailInfo && isShow && (
<OpenMail open={isShow} handleClose={onOk} fileInfo={mailInfo}/>
)}
{/* <LazyLoad onLoadMore={getMessages}></LazyLoad> */}
</>
)

View File

@ -19,7 +19,6 @@ import Tabs from '@mui/material/Tabs'
import Tab from '@mui/material/Tab'
import { useFetchMail } from '../../hooks/useFetchMail'
import { ShowMessage } from './ShowMessage'
import { fetchAndEvaluateMail } from '../../utils/fetchMail'
import { addToHashMapMail } from '../../state/features/mailSlice'
import MailIcon from '@mui/icons-material/Mail'
import {

View File

@ -38,7 +38,6 @@ import Tabs from '@mui/material/Tabs'
import Tab from '@mui/material/Tab'
import { useFetchMail } from '../../hooks/useFetchMail'
import { ShowMessage } from './ShowMessage'
import { fetchAndEvaluateMail } from '../../utils/fetchMail'
import { addToHashMapMail } from '../../state/features/mailSlice'
import { setIsLoadingGlobal } from '../../state/features/globalSlice'
import SimpleTable from './MailTable'
@ -46,6 +45,9 @@ import { AliasMail } from './AliasMail'
import { SentMail } from './SentMail'
import { NewThread } from './NewThread'
import { GroupMail } from './GroupMail'
import { useModal } from '../../components/common/useModal'
import { OpenMail } from './OpenMail'
import { MAIL_SERVICE_TYPE } from '../../constants/mail'
const steps: Step[] = [
{
@ -151,6 +153,7 @@ interface MailProps {
}
export const Mail = ({ isFromTo }: MailProps) => {
const {isShow, onCancel, onOk, show} = useModal()
const theme = useTheme()
const { user } = useSelector((state: RootState) => state.auth)
const [isOpen, setIsOpen] = useState<boolean>(false)
@ -167,6 +170,7 @@ export const Mail = ({ isFromTo }: MailProps) => {
const privateGroups = useSelector(
(state: RootState) => state.global.privateGroups
)
const [mailInfo, setMailInfo] = useState<any>(null)
const hasFetchedPrivateGroups = useSelector(
(state: RootState) => state.global.hasFetchedPrivateGroups
)
@ -238,25 +242,27 @@ export const Mail = ({ isFromTo }: MailProps) => {
content: any
) => {
try {
const existingMessage = hashMapMailMessages[messageIdentifier]
if (existingMessage) {
const existingMessage: any = hashMapMailMessages[messageIdentifier]
if (existingMessage && existingMessage.isValid && !existingMessage.unableToDecrypt) {
setMessage(existingMessage)
setIsOpen(true)
return
}
dispatch(setIsLoadingGlobal(true))
const res = await fetchAndEvaluateMail({
user,
messageIdentifier,
content,
otherUser: user
setMailInfo({
identifier: messageIdentifier,
name: user,
service: MAIL_SERVICE_TYPE
})
setMessage(res)
dispatch(addToHashMapMail(res))
setIsOpen(true)
const res: any = await show()
setMailInfo(null)
const existingMessageAgain = hashMapMailMessages[messageIdentifier]
if (res && res.isValid && !res.unableToDecrypt) {
setMessage(res)
setIsOpen(true)
return
}
} catch (error) {
} finally {
dispatch(setIsLoadingGlobal(false))
}
}
@ -689,6 +695,9 @@ export const Mail = ({ isFromTo }: MailProps) => {
showProgress={true}
showSkipButton={true}
/>
{mailInfo && isShow && (
<OpenMail open={isShow} handleClose={onOk} fileInfo={mailInfo}/>
)}
</Box>
)
}

View File

@ -226,7 +226,7 @@ export default function MailThread({
}}
>
<FileElement
fileInfo={file}
fileInfo={{...file, mimeTypeSaved: file?.type}}
title={file?.filename}
mode="mail"
otherUser={otherUser}

View File

@ -14,6 +14,7 @@ import CloseIcon from '@mui/icons-material/Close'
import CreateIcon from '@mui/icons-material/Create'
import { setNotification } from '../../state/features/notificationsSlice'
import { useNavigate, useLocation, useParams } from 'react-router-dom'
import mime from 'mime';
import {
objectToBase64,
@ -29,6 +30,7 @@ import {
} from '../../constants/mail'
import ConfirmationModal from '../../components/common/ConfirmationModal'
import useConfirmationModal from '../../hooks/useConfirmModal'
import { MultiplePublish } from '../../components/common/MultiplePublish/MultiplePublish'
const initialValue: Descendant[] = [
{
type: 'paragraph',
@ -53,6 +55,8 @@ export const NewMessage = ({
isFromTo
}: NewMessageProps) => {
const { name } = useParams()
const [publishes, setPublishes] = useState<any>(null);
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [isFromToName, setIsFromToName] = useState<null | string>(null)
const [isOpen, setIsOpen] = useState<boolean>(false)
const [value, setValue] = useState(initialValue)
@ -76,8 +80,45 @@ export const NewMessage = ({
const location = useLocation()
const { getRootProps, getInputProps } = useDropzone({
maxSize,
onDrop: (acceptedFiles) => {
setAttachments((prev) => [...prev, ...acceptedFiles])
onDrop: async (acceptedFiles) => {
let files: any[] = []
try {
acceptedFiles.forEach((item)=> {
const type = item?.type
if(!type){
files.push({
file: item,
mimetype: null,
extension: null
})
} else {
const extension = mime.getExtension(type);
if(!extension){
files.push({
file: item,
mimetype: type,
extension: null
})
} else {
files.push({
file: item,
mimetype: type,
extension: extension
})
}
}
})
} catch (error) {
dispatch(
setNotification({
msg: 'One of your files is corrupted',
alertType: 'error'
})
)
}
setAttachments((prev) => [...prev, ...files])
},
onDropRejected: (rejectedFiles) => {
dispatch(
@ -89,6 +130,8 @@ export const NewMessage = ({
}
})
console.log({attachments})
const openModal = () => {
setIsOpen(true)
@ -181,7 +224,11 @@ export const NewMessage = ({
if (alias && alias === aliasValue) {
errorMsg = "The recipient's alias cannot be the same as yours"
}
const noExtension = attachments.filter(item=> !item.extension)
if(noExtension.length > 0){
errorMsg = "One of your attachments does not have an extension (example: .png, .pdf, ect...)"
}
if (errorMsg) {
dispatch(
setNotification({
@ -191,7 +238,7 @@ export const NewMessage = ({
)
throw new Error(errorMsg)
}
if (aliasValue && !alias) {
const userConfirmed = await showModal()
if (userConfirmed === false) return
@ -244,7 +291,8 @@ export const NewMessage = ({
// START OF ATTACHMENT LOGIC
const attachmentArray = []
for (const attachment of attachments) {
for (const singleAttachment of attachments) {
const attachment = singleAttachment.file
const fileBase64 = await toBase64(attachment)
if (typeof fileBase64 !== 'string' || !fileBase64)
throw new Error('Could not convert file to base64')
@ -253,9 +301,9 @@ export const NewMessage = ({
const id = uid()
const id2 = uid()
const identifier = `attachments_qmail_${id}_${id2}`
const fileExtension = attachment?.name?.split('.')?.pop()
let fileExtension = attachment?.name?.split('.')?.pop()
if (!fileExtension) {
throw new Error('One of your attachments does not have an extension')
fileExtension = singleAttachment.extension
}
const obj = {
name: name,
@ -263,12 +311,14 @@ export const NewMessage = ({
filename: `${id}.${fileExtension}`,
originalFilename: attachment?.name || '',
identifier,
data64: base64String
data64: base64String,
type: attachment?.type
}
attachmentArray.push(obj)
}
const listOfPublishes = [...attachmentArray]
if (attachmentArray?.length > 0) {
mailObject.attachments = attachmentArray.map((item) => {
return {
@ -276,17 +326,18 @@ export const NewMessage = ({
name,
service: MAIL_ATTACHMENT_SERVICE_TYPE,
filename: item.filename,
originalFilename: item.originalFilename
originalFilename: item.originalFilename,
type: item?.type
}
})
const multiplePublish = {
action: 'PUBLISH_MULTIPLE_QDN_RESOURCES',
resources: [...attachmentArray],
encrypt: true,
publicKeys: [recipientPublicKey]
}
await qortalRequest(multiplePublish)
// const multiplePublish = {
// action: 'PUBLISH_MULTIPLE_QDN_RESOURCES',
// resources: [...attachmentArray],
// encrypt: true,
// publicKeys: [recipientPublicKey]
// }
// await qortalRequest(multiplePublish)
}
//END OF ATTACHMENT LOGIC
@ -313,15 +364,23 @@ export const NewMessage = ({
publicKeys: [recipientPublicKey]
}
await qortalRequest(requestBody)
dispatch(
setNotification({
msg: 'Message sent',
alertType: 'success'
})
)
// await qortalRequest(requestBody)
const multiplePublish = {
action: 'PUBLISH_MULTIPLE_QDN_RESOURCES',
resources: [...listOfPublishes, requestBody],
encrypt: true,
publicKeys: [recipientPublicKey]
};
setPublishes(multiplePublish);
setIsOpenMultiplePublish(true);
// dispatch(
// setNotification({
// msg: 'Message sent',
// alertType: 'success'
// })
// )
closeModal()
// closeModal()
} catch (error: any) {
let notificationObj = null
if (typeof error === 'string') {
@ -490,7 +549,7 @@ export const NewMessage = ({
></AttachFileIcon>
</Box>
<Box>
{attachments.map((file, index) => {
{attachments.map(({file, extension}, index) => {
return (
<Box
sx={{
@ -501,7 +560,8 @@ export const NewMessage = ({
>
<Typography
sx={{
fontSize: '16px'
fontSize: '16px',
color: !extension ? 'red' : 'unset'
}}
>
{file?.name}
@ -518,6 +578,18 @@ export const NewMessage = ({
cursor: 'pointer'
}}
/>
{!extension && (
<Typography
sx={{
fontSize: '12px',
fontWeight: 'bold',
color: 'red'
}}
>
This file has no extension
</Typography>
)}
</Box>
)
})}
@ -537,6 +609,23 @@ export const NewMessage = ({
<BuilderButton onClick={closeModal}>Close</BuilderButton>
</ReusableModal>
<Modal />
{isOpenMultiplePublish && (
<MultiplePublish
isOpen={isOpenMultiplePublish}
onSubmit={() => {
dispatch(
setNotification({
msg: 'Message sent',
alertType: 'success'
})
)
setIsOpenMultiplePublish(false);
setPublishes(null)
closeModal()
}}
publishes={publishes}
/>
)}
</Box>
)
}

View File

@ -14,6 +14,7 @@ import CloseIcon from '@mui/icons-material/Close'
import CreateIcon from '@mui/icons-material/Create'
import { setNotification } from '../../state/features/notificationsSlice'
import { useNavigate, useLocation } from 'react-router-dom'
import mime from 'mime';
import {
objectToBase64,
@ -71,7 +72,43 @@ export const NewThread = ({
const { getRootProps, getInputProps } = useDropzone({
maxSize,
onDrop: (acceptedFiles) => {
setAttachments((prev) => [...prev, ...acceptedFiles])
let files: any[] = []
try {
acceptedFiles.forEach((item)=> {
const type = item?.type
if(!type){
files.push({
file: item,
mimetype: null,
extension: null
})
} else {
const extension = mime.getExtension(type);
if(!extension){
files.push({
file: item,
mimetype: type,
extension: null
})
} else {
files.push({
file: item,
mimetype: type,
extension: extension
})
}
}
})
} catch (error) {
dispatch(
setNotification({
msg: 'One of your files is corrupted',
alertType: 'error'
})
)
}
setAttachments((prev) => [...prev, ...files])
},
onDropRejected: (rejectedFiles) => {
dispatch(
@ -119,6 +156,10 @@ export const NewThread = ({
const errMsg = `Missing: ${missingFieldsString}`
errorMsg = errMsg
}
const noExtension = attachments.filter(item=> !item.extension)
if(noExtension.length > 0){
errorMsg = "One of your attachments does not have an extension (example: .png, .pdf, ect...)"
}
if (errorMsg) {
dispatch(
@ -151,7 +192,9 @@ export const NewThread = ({
// START OF ATTACHMENT LOGIC
const attachmentArray: any[] = []
for (const attachment of attachments) {
for (const singleAttachment of attachments) {
const attachment = singleAttachment.file
const fileBase64 = await toBase64(attachment)
if (typeof fileBase64 !== 'string' || !fileBase64)
throw new Error('Could not convert file to base64')
@ -160,9 +203,9 @@ export const NewThread = ({
const id = uid()
const id2 = uid()
const identifier = `attachments_qmail_${id}_${id2}`
const fileExtension = attachment?.name?.split('.')?.pop()
let fileExtension = attachment?.name?.split('.')?.pop()
if (!fileExtension) {
throw new Error('One of your attachments does not have an extension')
fileExtension = singleAttachment.extension
}
const obj = {
name: name,
@ -170,7 +213,8 @@ export const NewThread = ({
filename: `${id}.${fileExtension}`,
originalFilename: attachment?.name || '',
identifier,
data64: base64String
data64: base64String,
type: attachment?.type
}
attachmentArray.push(obj)
@ -183,7 +227,8 @@ export const NewThread = ({
name,
service: MAIL_ATTACHMENT_SERVICE_TYPE,
filename: item.filename,
originalFilename: item.originalFilename
originalFilename: item.originalFilename,
type: item?.type
}
})
@ -404,7 +449,7 @@ export const NewThread = ({
></AttachFileIcon>
</Box>
<Box>
{attachments.map((file, index) => {
{attachments.map(({file, extension}, index) => {
return (
<Box
sx={{
@ -415,7 +460,8 @@ export const NewThread = ({
>
<Typography
sx={{
fontSize: '16px'
fontSize: '16px',
color: !extension ? 'red' : 'unset'
}}
>
{file?.name}
@ -432,6 +478,17 @@ export const NewThread = ({
cursor: 'pointer'
}}
/>
{!extension && (
<Typography
sx={{
fontSize: '12px',
fontWeight: 'bold',
color: 'red'
}}
>
This file has no extension
</Typography>
)}
</Box>
)
})}

302
src/pages/Mail/OpenMail.tsx Normal file
View File

@ -0,0 +1,302 @@
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Typography,
} from "@mui/material";
import React, { useEffect } from "react";
import { MyContext } from "../../wrappers/DownloadWrapper";
import { RootState } from "../../state/store";
import { useDispatch, useSelector } from "react-redux";
import { setNotification } from "../../state/features/notificationsSlice";
import { fetchAndEvaluateMail } from "../../utils/fetchMail";
import { addToHashMapMail } from "../../state/features/mailSlice";
interface OpenMailProps {
open: boolean;
handleClose: (payload?:any) => void;
children?: React.ReactNode;
fileInfo?: any;
mimeTypeSaved?: string;
disable?: boolean;
mode?: string;
otherUser?: string;
customStyles?: any;
}
export const OpenMail = ({
open,
handleClose,
fileInfo
}: OpenMailProps) => {
const { downloadVideo } = React.useContext(MyContext);
const [startedDownload, setStartedDownload] = React.useState<boolean>(false);
const [isLoading, setIsLoading] = React.useState<boolean>(false);
const downloads = useSelector((state: RootState) => state.global?.downloads);
const hasCommencedDownload = React.useRef(false);
const dispatch = useDispatch();
const reDownload = React.useRef<boolean>(false);
const status = React.useRef<null | string>(null);
const [unableToDecrypt, setUnableToDecrypt] = React.useState<boolean>(false)
const [isValid, setIsValid] = React.useState<boolean>(true)
const saveToHash = (payload: any)=> {
console.log({payload})
dispatch(addToHashMapMail(payload))
}
const handleFetchMail = async (contentInfo: any, saveToHashFunc: any)=> {
try {
const res = await fetchAndEvaluateMail(contentInfo, saveToHashFunc)
console.log({res})
if(res.unableToDecrypt){
setUnableToDecrypt(true)
} else if(res.isValid === false){
setIsValid(false)
} else {
handleClose(res)
}
} catch (error) {
}
}
const isFetchingProperties = React.useRef<boolean>(false);
const download = React.useMemo(() => {
if (!downloads || !fileInfo?.identifier) return {};
const findDownload = downloads[fileInfo?.identifier];
if (!findDownload) return {};
return findDownload;
}, [downloads, fileInfo]);
const resourceStatus = React.useMemo(() => {
return download?.status || {};
}, [download]);
console.log({resourceStatus})
const handleDownloadMail = async () => {
const { name, service, identifier } = fileInfo;
try {
const res = await qortalRequest({
action: 'GET_QDN_RESOURCE_STATUS',
name: name,
service: service,
identifier: identifier
})
if(res?.status === "READY"){
hasCommencedDownload.current = true;
handleFetchMail({
user: name,
messageIdentifier: identifier,
content: fileInfo,
otherUser: name
}, saveToHash)
return
}
} catch (error) {
}
setStartedDownload(true);
if (resourceStatus?.status === "READY" && !hasCommencedDownload.current) {
hasCommencedDownload.current = true;
handleFetchMail({
user: name,
messageIdentifier: identifier,
content: fileInfo,
otherUser: name
}, saveToHash)
}
setIsLoading(true);
downloadVideo({
name,
service,
identifier,
properties: {}
});
};
const refetch = React.useCallback(async () => {
if (!fileInfo) return;
try {
const { name, service, identifier } = fileInfo;
isFetchingProperties.current = true;
await qortalRequest({
action: "GET_QDN_RESOURCE_PROPERTIES",
name,
service,
identifier,
});
} catch (error) {
} finally {
isFetchingProperties.current = false;
}
}, [fileInfo]);
const refetchInInterval = () => {
try {
const interval = setInterval(() => {
if (status?.current === "DOWNLOADED") {
refetch();
}
if (status?.current === "READY") {
clearInterval(interval);
}
}, 7500);
} catch (error) {}
};
React.useEffect(() => {
if (resourceStatus?.status) {
status.current = resourceStatus?.status;
}
if (
resourceStatus?.status === "READY" && !hasCommencedDownload.current
) {
const { name, service, identifier } = fileInfo;
hasCommencedDownload.current = true;
handleFetchMail({
user: name,
messageIdentifier: identifier,
content: fileInfo,
otherUser: name
}, saveToHash)
} else if (
resourceStatus?.status === "DOWNLOADED" &&
reDownload?.current === false
) {
refetchInInterval();
reDownload.current = true;
}
}, [resourceStatus, download]);
useEffect(()=> {
handleDownloadMail()
}, [])
return (
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Mail download status</DialogTitle>
<DialogContent>
{unableToDecrypt && (
<Typography
variant="subtitle2"
component="div"
sx={{
fontSize: "14px",
}}
>
Unable to decrypt message
</Typography>
)}
{!isValid && (
<Typography
variant="subtitle2"
component="div"
sx={{
fontSize: "14px",
}}
>
Message has an invalid format
</Typography>
)}
{!resourceStatus.status && (isValid && !unableToDecrypt) && (
<Box
display="flex"
justifyContent="center"
alignItems="center"
sx={{
display: "flex",
flexDirection: "column",
gap: "10px",
padding: "8px",
borderRadius: "10px",
}}
>
<CircularProgress color="secondary" />
<>Downloading Message</>
</Box>
)}
{((resourceStatus.status && resourceStatus?.status !== "READY") && (isValid && !unableToDecrypt)) && (
<Box
display="flex"
justifyContent="center"
alignItems="center"
sx={{
display: "flex",
flexDirection: "column",
gap: "10px",
padding: "8px",
borderRadius: "10px",
}}
>
<CircularProgress color="secondary" />
{resourceStatus && (
<Typography
variant="subtitle2"
component="div"
sx={{
color: "white",
fontSize: "14px",
}}
>
{resourceStatus?.status === "REFETCHING" ? (
<>
<>
{(
(resourceStatus?.localChunkCount /
resourceStatus?.totalChunkCount) *
100
)?.toFixed(0)}
%
</>
<> Refetching in 2 minutes</>
</>
) : resourceStatus?.status === "DOWNLOADED" ? (
<>Download Completed: building message...</>
) : resourceStatus?.status === "DOWNLOADING" ? (
<>Downloading Message</>
) : resourceStatus?.status !== "READY" ? (
<>
{(
(resourceStatus?.localChunkCount /
resourceStatus?.totalChunkCount) *
100
)?.toFixed(0)}
%
</>
) : (
<>Download Completed: fetching message...</>
)}
</Typography>
)}
</Box>
)}
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={handleClose}>
Close
</Button>
</DialogActions>
</Dialog>
);
};

View File

@ -19,7 +19,6 @@ import Tabs from '@mui/material/Tabs'
import Tab from '@mui/material/Tab'
import { useFetchMail } from '../../hooks/useFetchMail'
import { ShowMessage } from './ShowMessage'
import { fetchAndEvaluateMail } from '../../utils/fetchMail'
import { addToHashMapMail } from '../../state/features/mailSlice'
import {
setIsLoadingGlobal,
@ -29,9 +28,13 @@ import SimpleTable from './MailTable'
import { MAIL_SERVICE_TYPE } from '../../constants/mail'
import { BlogPost } from '../../state/features/blogSlice'
import { setNotification } from '../../state/features/notificationsSlice'
import { useModal } from '../../components/common/useModal'
import { OpenMail } from './OpenMail'
interface SentMailProps {}
export const SentMail = ({}: SentMailProps) => {
const {isShow, onCancel, onOk, show} = useModal()
const theme = useTheme()
const { user } = useSelector((state: RootState) => state.auth)
const [isOpen, setIsOpen] = useState<boolean>(false)
@ -40,6 +43,7 @@ export const SentMail = ({}: SentMailProps) => {
const [valueTab, setValueTab] = React.useState(0)
const [aliasValue, setAliasValue] = useState('')
const [alias, setAlias] = useState<string[]>([])
const [mailInfo, setMailInfo] = useState<any>(null)
const hashMapPosts = useSelector(
(state: RootState) => state.blog.hashMapPosts
)
@ -274,24 +278,25 @@ export const SentMail = ({}: SentMailProps) => {
content: any
) => {
try {
const existingMessage = hashMapMailMessages[messageIdentifier]
if (existingMessage) {
const existingMessage: any = hashMapMailMessages[messageIdentifier]
if (existingMessage && existingMessage.isValid && !existingMessage.unableToDecrypt) {
setMessage(existingMessage)
setIsOpen(true)
return
}
dispatch(setIsLoadingGlobal(true))
// const findUser = await findUserFunc(messageIdentifier)
// if (!findUser) throw new Error('cannot find user')
const res = await fetchAndEvaluateMail({
user,
messageIdentifier,
content,
otherUser: user
setMailInfo({
identifier: messageIdentifier,
name: user,
service: MAIL_SERVICE_TYPE
})
setMessage(res)
dispatch(addToHashMapMail(res))
setIsOpen(true)
const res: any = await show()
setMailInfo(null)
const existingMessageAgain = hashMapMailMessages[messageIdentifier]
if (res && res.isValid && !res.unableToDecrypt) {
setMessage(res)
setIsOpen(true)
return
}
} catch (error) {
dispatch(
setNotification({
@ -300,12 +305,14 @@ export const SentMail = ({}: SentMailProps) => {
})
)
} finally {
dispatch(setIsLoadingGlobal(false))
}
}
return (
<>
{mailInfo && isShow && (
<OpenMail open={isShow} handleClose={onOk} fileInfo={mailInfo}/>
)}
<NewMessage replyTo={replyTo} setReplyTo={setReplyTo} hideButton />
<ShowMessage
isOpen={isOpen}

View File

@ -63,6 +63,7 @@ export const ShowMessage = ({
cleanHTML = DOMPurify.sanitize(message.htmlContent);
}
console.log({message})
return (
<Box
sx={{
@ -216,7 +217,7 @@ export const ShowMessage = ({
}}
>
<FileElement
fileInfo={file}
fileInfo={{...file, mimeTypeSaved: file?.type}}
title={file?.filename}
mode="mail"
otherUser={message?.user}

View File

@ -136,7 +136,7 @@ export const ShowMessage = ({ message }: any) => {
}}
>
<FileElement
fileInfo={file}
fileInfo={{...file, mimeTypeSaved: file?.type}}
title={file?.filename}
mode="mail"
otherUser={message?.name}

View File

@ -7,7 +7,7 @@ import {
uint8ArrayToObject
} from './toBase64'
export const fetchAndEvaluateMail = async (data: any) => {
export const fetchAndEvaluateMail = async (data: any, saveToHash?: (val: any)=> void) => {
const getBlogPost = async () => {
const { user, messageIdentifier, content, otherUser } = data
let obj: any = {
@ -45,12 +45,30 @@ export const fetchAndEvaluateMail = async (data: any) => {
encryptedData: base64,
publicKey: recipientPublicKey
}
const resDecrypt = await qortalRequest(requestEncryptBody)
if (!resDecrypt) return obj
let unableToDecrypt = true
let resDecrypt = null
try {
resDecrypt = await qortalRequest(requestEncryptBody)
unableToDecrypt = false
} catch (error) {
}
console.log({resDecrypt})
if (!resDecrypt){
obj = {
...obj,
unableToDecrypt: true,
id: messageIdentifier,
user
}
if(saveToHash){
saveToHash(obj)
}
return obj
}
const decryptToUnit8Array = base64ToUint8Array(resDecrypt)
const responseData = uint8ArrayToObject(decryptToUnit8Array)
console.log({responseData}, checkStructureMailMessages(responseData))
if (checkStructureMailMessages(responseData)) {
obj = {
...content,
@ -62,6 +80,9 @@ export const fetchAndEvaluateMail = async (data: any) => {
isValid: true
}
}
if(saveToHash){
saveToHash(obj)
}
return obj
} catch (error) {
console.log({ error })

View File

@ -1,31 +1,19 @@
import React, { useState } from 'react'
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import { addUser } from '../state/features/authSlice'
import ShortUniqueId from 'short-unique-id'
import { RootState } from '../state/store'
import PublishBlogModal from '../components/modals/PublishBlogModal'
import EditBlogModal from '../components/modals/EditBlogModal'
import {
setAddToDownloads,
setCurrentBlog,
setIsLoadingGlobal,
toggleEditBlogModal,
togglePublishBlogModal,
updateDownloads
} from '../state/features/globalSlice'
import { useFetchPosts } from '../hooks/useFetchPosts'
import { setNotification } from '../state/features/notificationsSlice'
import { DownloadTaskManager } from '../components/common/DownloadTaskManager'
import { RootState } from '../state/store'
interface Props {
children: React.ReactNode
}
const uid = new ShortUniqueId()
const defaultValues: MyContextInterface = {
downloadVideo: () => {}
@ -34,48 +22,22 @@ interface IDownloadVideoParams {
name: string
service: string
identifier: string
blogPost: any
properties: any
}
interface MyContextInterface {
downloadVideo: ({
name,
service,
identifier,
blogPost
properties
}: IDownloadVideoParams) => void
}
export const MyContext = React.createContext<MyContextInterface>(defaultValues)
const DownloadWrapper: React.FC<Props> = ({ children }) => {
const navigate = useNavigate()
const dispatch = useDispatch()
const { user } = useSelector((state: RootState) => state.auth)
const { audios, currAudio } = useSelector((state: RootState) => state.global)
const { getBlogPosts } = useFetchPosts()
const { downloads, currentBlog, isLoadingGlobal, isOpenEditBlogModal } =
useSelector((state: RootState) => state.global)
const downloads = useSelector((state: RootState) => state.global?.downloads);
const addToPile = async ({
name,
service,
identifier,
blogPost
}: IDownloadVideoParams) => {
try {
const res = await qortalRequest({
action: 'GET_QDN_RESOURCE_STATUS',
name: name,
service: service,
identifier: identifier
})
if (
res?.status === 'READY' ||
(res.percentLoaded > 75 && res.totalChunkCount < 100)
) {
return false
}
} catch (error) {}
}
const fetchResource = async ({ name, service, identifier }: any) => {
try {
@ -114,21 +76,21 @@ const DownloadWrapper: React.FC<Props> = ({ children }) => {
name,
service,
identifier,
blogPost
properties
}: IDownloadVideoParams) => {
if(downloads[identifier]) return
dispatch(
setAddToDownloads({
name,
service,
identifier,
blogPost
properties
})
)
const url = `/arbitrary/${service}/${name}/${identifier}`
let isCalling = false
let percentLoaded = 0
let timer = 25
let timer = 24
const intervalId = setInterval(async () => {
if (isCalling) return
isCalling = true
@ -138,6 +100,17 @@ const DownloadWrapper: React.FC<Props> = ({ children }) => {
service: service,
identifier: identifier
})
if(res?.status === 'NOT_PUBLISHED'){
dispatch(
updateDownloads({
name,
service,
identifier,
status: res
})
)
clearInterval(intervalId)
}
isCalling = false
if (res.localChunkCount) {
if (res.percentLoaded) {
@ -145,12 +118,12 @@ const DownloadWrapper: React.FC<Props> = ({ children }) => {
res.percentLoaded === percentLoaded &&
res.percentLoaded !== 100
) {
timer = timer - 3
timer = timer - 5
} else {
timer = 25
timer = 24
}
if (timer < 0) {
timer = 25
timer = 24
isCalling = true
dispatch(
updateDownloads({
@ -170,7 +143,7 @@ const DownloadWrapper: React.FC<Props> = ({ children }) => {
service,
identifier
})
}, 120000)
}, 25000)
return
}
percentLoaded = res.percentLoaded
@ -197,21 +170,20 @@ const DownloadWrapper: React.FC<Props> = ({ children }) => {
})
)
}
}, 3000) // 1 second interval
}, 5000) // 1 second interval
fetchVideoUrl({
name,
service,
identifier
})
}
const downloadVideo = async ({
name,
service,
identifier,
blogPost
properties
}: IDownloadVideoParams) => {
try {
@ -220,7 +192,7 @@ const DownloadWrapper: React.FC<Props> = ({ children }) => {
name,
service,
identifier,
blogPost
properties
})
return 'addedToList'
} catch (error) {
@ -231,7 +203,7 @@ const DownloadWrapper: React.FC<Props> = ({ children }) => {
return (
<>
<MyContext.Provider value={{ downloadVideo }}>
<DownloadTaskManager />
{/* <DownloadTaskManager /> */}
{children}
</MyContext.Provider>
</>

View File

@ -0,0 +1,208 @@
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import {
setAddToDownloads,
updateDownloads
} from '../state/features/globalSlice'
import { RootState } from '../state/store'
interface Props {
children: React.ReactNode
}
const defaultValues: MyContextInterface = {
downloadVideo: () => {}
}
interface IDownloadVideoParams {
name: string
service: string
identifier: string
properties: any
}
interface MyContextInterface {
downloadVideo: ({
name,
service,
identifier,
properties
}: IDownloadVideoParams) => void
}
export const MyContext = React.createContext<MyContextInterface>(defaultValues)
export const MailDownloadWrapper: React.FC<Props> = ({ children }) => {
const dispatch = useDispatch()
const downloads = useSelector((state: RootState) => state.global?.downloads);
const fetchResource = async ({ name, service, identifier }: any) => {
try {
await qortalRequest({
action: 'GET_QDN_RESOURCE_PROPERTIES',
name,
service,
identifier
})
} catch (error) {}
}
const fetchVideoUrl = async ({ name, service, identifier }: any) => {
try {
fetchResource({ name, service, identifier })
let url = await qortalRequest({
action: 'GET_QDN_RESOURCE_URL',
service: service,
name: name,
identifier: identifier
})
if (url) {
dispatch(
updateDownloads({
name,
service,
identifier,
url
})
)
}
} catch (error) {}
}
const performDownload = ({
name,
service,
identifier,
properties
}: IDownloadVideoParams) => {
if(downloads[identifier]) return;
dispatch(
setAddToDownloads({
name,
service,
identifier,
properties
})
)
let isCalling = false
let percentLoaded = 0
let timer = 24
const intervalId = setInterval(async () => {
if (isCalling) return
isCalling = true
const res = await qortalRequest({
action: 'GET_QDN_RESOURCE_STATUS',
name: name,
service: service,
identifier: identifier
})
if(res?.status === 'NOT_PUBLISHED'){
dispatch(
updateDownloads({
name,
service,
identifier,
status: res
})
)
clearInterval(intervalId)
}
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
dispatch(
updateDownloads({
name,
service,
identifier,
status: {
...res,
status: 'REFETCHING'
}
})
)
setTimeout(() => {
isCalling = false
fetchResource({
name,
service,
identifier
})
}, 25000)
return
}
percentLoaded = res.percentLoaded
}
dispatch(
updateDownloads({
name,
service,
identifier,
status: res
})
)
}
// check if progress is 100% and clear interval if true
if (res?.status === 'READY') {
clearInterval(intervalId)
dispatch(
updateDownloads({
name,
service,
identifier,
status: res
})
)
}
}, 5000) // 1 second interval
fetchVideoUrl({
name,
service,
identifier
})
}
const downloadVideo = async ({
name,
service,
identifier,
properties
}: IDownloadVideoParams) => {
try {
performDownload({
name,
service,
identifier,
properties
})
return 'addedToList'
} catch (error) {
console.error(error)
}
}
return (
<>
<MyContext.Provider value={{ downloadVideo }}>
{children}
</MyContext.Provider>
</>
)
}

View File

@ -1,27 +1,13 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import legacy from '@vitejs/plugin-legacy'
import react from '@vitejs/plugin-react'
export default defineConfig(({ mode }) => {
const config = {
build: {
target: 'es2015'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: "",
build: {
commonjsOptions: {
esmExternals: true
},
plugins: [
react(),
legacy({
targets: [
'chrome >= 64',
'edge >= 79',
'safari >= 11.1',
'firefox >= 67'
],
ignoreBrowserslistConfig: true,
renderLegacyChunks: false,
modernPolyfills: ['es/global-this']
})
],
base: ''
}
return config
},
})