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:
parent
3e71890f7b
commit
3dbb791eba
4045
package-lock.json
generated
4045
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
}
|
||||
}
|
||||
|
23
src/assets/svgs/CircleSVG.tsx
Normal file
23
src/assets/svgs/CircleSVG.tsx
Normal 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>
|
||||
);
|
||||
};
|
23
src/assets/svgs/EmptyCircleSVG.tsx
Normal file
23
src/assets/svgs/EmptyCircleSVG.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
201
src/components/common/MultiplePublish/MultiplePublish.tsx
Normal file
201
src/components/common/MultiplePublish/MultiplePublish.tsx
Normal 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",
|
||||
},
|
||||
}));
|
46
src/components/common/useModal.tsx
Normal file
46
src/components/common/useModal.tsx
Normal 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
|
||||
};
|
||||
};
|
@ -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',
|
||||
|
@ -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> */}
|
||||
</>
|
||||
)
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -226,7 +226,7 @@ export default function MailThread({
|
||||
}}
|
||||
>
|
||||
<FileElement
|
||||
fileInfo={file}
|
||||
fileInfo={{...file, mimeTypeSaved: file?.type}}
|
||||
title={file?.filename}
|
||||
mode="mail"
|
||||
otherUser={otherUser}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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
302
src/pages/Mail/OpenMail.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 })
|
||||
|
@ -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>
|
||||
</>
|
||||
|
208
src/wrappers/MailDownloadWrapper.tsx
Normal file
208
src/wrappers/MailDownloadWrapper.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user