Removed most code from Mail.tsx.

Mail.tsx renamed to Home.tsx and moved to its own directory. It shows a "New Forum" button if the user owns the app or Test Identifiers are being used.
This commit is contained in:
Qortal Dev 2024-08-26 16:20:21 -06:00
parent aaf9923103
commit c1d812a0e6
17 changed files with 861 additions and 1168 deletions

View File

@ -1,19 +1,19 @@
// @ts-nocheck // @ts-nocheck
import { Routes, Route } from 'react-router-dom' import { Routes, Route } from "react-router-dom";
import { ThemeProvider } from '@mui/material/styles' import { ThemeProvider } from "@mui/material/styles";
import { CssBaseline } from '@mui/material' import { CssBaseline } from "@mui/material";
import { lightTheme, darkTheme } from './styles/theme' import { Home } from "./pages/Home/Home";
import { store } from './state/store' import { lightTheme, darkTheme } from "./styles/theme";
import { Provider } from 'react-redux' import { store } from "./state/store";
import GlobalWrapper from './wrappers/GlobalWrapper' import { Provider } from "react-redux";
import DownloadWrapper from './wrappers/DownloadWrapper' import GlobalWrapper from "./wrappers/GlobalWrapper";
import Notification from './components/common/Notification/Notification' import DownloadWrapper from "./wrappers/DownloadWrapper";
import { Mail } from './pages/Mail/Mail' import Notification from "./components/common/Notification/Notification";
function App() { function App() {
const themeColor = window._qdnTheme const themeColor = window._qdnTheme;
return ( return (
<Provider store={store}> <Provider store={store}>
@ -24,14 +24,13 @@ function App() {
<CssBaseline /> <CssBaseline />
<Routes> <Routes>
<Route path="/" element={<Mail />} /> <Route path="/" element={<Home />} />
<Route path="/to/:name" element={<Mail isFromTo />} />
</Routes> </Routes>
</GlobalWrapper> </GlobalWrapper>
</DownloadWrapper> </DownloadWrapper>
</ThemeProvider> </ThemeProvider>
</Provider> </Provider>
) );
} }
export default App export default App;

View File

@ -1,8 +1,11 @@
const useTestIdentifiers = false; export const useTestIdentifiers = true;
const appName = useTestIdentifiers ? "mintTEST" : "mintership"; const appName = useTestIdentifiers ? "mintTEST" : "mintership";
export const AUDIO_BASE = `${appName}_qaudio`;
export const THREAD_BASE = `${appName}_forum_thread`;
export const ATTATCHMENT_BASE = `${appName}_attachments`;
export const QMAIL_BASE = `${appName}_mail`; export const QMAIL_BASE = `${appName}_mail`;
export const ATTATCHMENT_BASE = `${appName}_attachments`;
export const AUDIO_BASE = `${appName}_qaudio`;
export const FORUM_BASE = `${appName}_forum`;
export const THREAD_BASE = `${appName}_thread`;
export const THREAD_MESSAGE = `${appName}_thread_message`; export const THREAD_MESSAGE = `${appName}_thread_message`;

1
src/constants/Misc.ts Normal file
View File

@ -0,0 +1 @@
export const appOwner = "Q-Mintership";

47
src/pages/Forum/Forum.tsx Normal file
View File

@ -0,0 +1,47 @@
import { Box } from "@mui/material";
import React, { useState } from "react";
import ComposeIconSVG from "../../assets/svgs/ComposeIcon.svg";
import { executeEvent } from "../../utils/events";
import {
ComposeContainer,
ComposeIcon,
ComposeP,
InstanceContainer,
} from "../Home/Home-styles";
import { GroupMail } from "../Mail/GroupMail";
export type EncryptionType = "None" | "Group" | "GroupAdmin";
interface ForumProps {
title: string;
description: string;
encryption?: EncryptionType;
}
export const Forum = ({ encryption = "None" }: ForumProps) => {
const [currentThread, setCurrentThread] = useState<any>(null);
const openNewThread = () => {
if (currentThread) {
executeEvent("openNewThreadMessageModal", {});
return;
}
executeEvent("openNewThreadModal", {});
};
return (
<Box>
<InstanceContainer>
<ComposeContainer onClick={openNewThread}>
<ComposeIcon src={ComposeIconSVG} />
<ComposeP>{currentThread ? "New Post" : "New Thread"}</ComposeP>
</ComposeContainer>
</InstanceContainer>
{/*<GroupMail*/}
{/* groupInfo={selectedGroup}*/}
{/* currentThread={currentThread}*/}
{/* setCurrentThread={setCurrentThread}*/}
{/*/>*/}
</Box>
);
};

View File

@ -1,7 +1,107 @@
import React from 'react' import { Box } from "@mui/material";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useSelector } from "react-redux";
import ComposeIconSVG from "../../assets/svgs/ComposeIcon.svg";
import { useTestIdentifiers } from "../../constants/Identifiers";
import { appOwner } from "../../constants/Misc";
import { useFetchMail } from "../../hooks/useFetchMail";
import { RootState } from "../../state/store";
import { executeEvent } from "../../utils/events";
import {
ComposeContainer,
ComposeIcon,
ComposeP,
InstanceContainer,
MailContainer,
} from "./Home-styles";
export const Home = () => { export const Home = () => {
const { user } = useSelector((state: RootState) => state.auth);
const privateGroups = useSelector(
(state: RootState) => state.global.privateGroups
);
const options = useMemo(() => {
return Object.keys(privateGroups).map(key => {
return {
...privateGroups[key],
name: privateGroups[key].groupName,
id: key,
};
});
}, [privateGroups]);
const hashMapMailMessages = useSelector(
(state: RootState) => state.mail.hashMapMailMessages
);
const userName = useMemo(() => {
if (!user?.name) return "";
return user.name;
}, [user]);
const { getMailMessages, checkNewMessages } = useFetchMail();
const getMessages = React.useCallback(
async (isOnMount?: boolean) => {
if (!user?.name || !user?.address) return;
try {
await getMailMessages(user.name, user.address);
} catch (error) {}
},
[getMailMessages, user]
);
const interval = useRef<any>(null);
const checkNewMessagesFunc = useCallback(() => {
if (!user?.name || !user?.address) return;
let isCalling = false;
interval.current = setInterval(async () => {
if (isCalling || !user?.name || !user?.address) return;
isCalling = true;
const res = await checkNewMessages(user?.name, user.address);
isCalling = false;
}, 30000);
}, [checkNewMessages, user]);
useEffect(() => {
checkNewMessagesFunc();
return () => {
if (interval?.current) {
clearInterval(interval.current);
}
};
}, [checkNewMessagesFunc]);
const firstMount = useRef(false);
useEffect(() => {
if (user?.name && !firstMount.current) {
getMessages(true);
firstMount.current = true;
}
}, [user]);
const publishNewForum = () => {
executeEvent("openNewThreadModal", {});
};
return ( return (
<div>Home</div> <Box>
) <InstanceContainer>
} {(user?.name === appOwner || useTestIdentifiers) && (
<ComposeContainer onClick={publishNewForum}>
<ComposeIcon src={ComposeIconSVG} />
<ComposeP>{"New Forum"}</ComposeP>
</ComposeContainer>
)}
</InstanceContainer>
</Box>
);
};

View File

@ -4,112 +4,116 @@ import React, {
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
useState useState,
} from 'react' } from "react";
import { useNavigate } from 'react-router-dom' import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from "react-redux";
import { RootState } from '../../state/store' import { RootState } from "../../state/store";
import EditIcon from '@mui/icons-material/Edit' import EditIcon from "@mui/icons-material/Edit";
import { Box, Button, Input, Typography, useTheme } from '@mui/material' import { Box, Button, Input, Typography, useTheme } from "@mui/material";
import { useFetchPosts } from '../../hooks/useFetchPosts' import { useFetchPosts } from "../../hooks/useFetchPosts";
import LazyLoad from '../../components/common/LazyLoad' import LazyLoad from "../../components/common/LazyLoad";
import { removePrefix } from '../../utils/blogIdformats' import { removePrefix } from "../../utils/blogIdformats";
import { NewMessage } from './NewMessage' import { NewMessage } from "./NewMessage";
import Tabs from '@mui/material/Tabs' import Tabs from "@mui/material/Tabs";
import Tab from '@mui/material/Tab' import Tab from "@mui/material/Tab";
import { useFetchMail } from '../../hooks/useFetchMail' import { useFetchMail } from "../../hooks/useFetchMail";
import { ShowMessage } from './ShowMessage' import { ShowMessage } from "./ShowMessage";
import { addToHashMapMail } from '../../state/features/mailSlice' import { addToHashMapMail } from "../../state/features/mailSlice";
import { import {
setIsLoadingGlobal, setIsLoadingGlobal,
setUserAvatarHash setUserAvatarHash,
} from '../../state/features/globalSlice' } from "../../state/features/globalSlice";
import SimpleTable from './MailTable' import SimpleTable from "./MailTable";
import { MAIL_SERVICE_TYPE } from '../../constants/mail' import { MAIL_SERVICE_TYPE } from "../../constants/mail";
import { BlogPost } from '../../state/features/blogSlice' import { BlogPost } from "../../state/features/blogSlice";
import { useModal } from '../../components/common/useModal' import { useModal } from "../../components/common/useModal";
import { OpenMail } from './OpenMail' import { OpenMail } from "./OpenMail";
import { MessagesContainer } from './Mail-styles' import { MessagesContainer } from "../Home/Home-styles";
import { MailMessageRow } from './MailMessageRow' import { MailMessageRow } from "./MailMessageRow";
interface AliasMailProps { interface AliasMailProps {
value: string; value: string;
onOpen: (user: string, identifier: string, content: any)=> Promise<void> onOpen: (user: string, identifier: string, content: any) => Promise<void>;
messageOpenedId: number messageOpenedId: number;
} }
export const AliasMail = ({ value, onOpen, messageOpenedId}: AliasMailProps) => { export const AliasMail = ({
const {isShow, onCancel, onOk, show} = useModal() value,
onOpen,
messageOpenedId,
}: AliasMailProps) => {
const { isShow, onCancel, onOk, show } = useModal();
const theme = useTheme() const theme = useTheme();
const { user } = useSelector((state: RootState) => state.auth) const { user } = useSelector((state: RootState) => state.auth);
const [isOpen, setIsOpen] = useState<boolean>(false) const [isOpen, setIsOpen] = useState<boolean>(false);
const [message, setMessage] = useState<any>(null) const [message, setMessage] = useState<any>(null);
const [replyTo, setReplyTo] = useState<any>(null) const [replyTo, setReplyTo] = useState<any>(null);
const [forwardInfo, setForwardInfo] = useState<any>(null); const [forwardInfo, setForwardInfo] = useState<any>(null);
const [valueTab, setValueTab] = React.useState(0) const [valueTab, setValueTab] = React.useState(0);
const [aliasValue, setAliasValue] = useState('') const [aliasValue, setAliasValue] = useState("");
const [alias, setAlias] = useState<string[]>([]) const [alias, setAlias] = useState<string[]>([]);
const [mailInfo, setMailInfo] = useState<any>(null) const [mailInfo, setMailInfo] = useState<any>(null);
const hashMapPosts = useSelector( const hashMapPosts = useSelector(
(state: RootState) => state.blog.hashMapPosts (state: RootState) => state.blog.hashMapPosts
) );
const [mailMessages, setMailMessages] = useState<any[]>([]) const [mailMessages, setMailMessages] = useState<any[]>([]);
const hashMapMailMessages = useSelector( const hashMapMailMessages = useSelector(
(state: RootState) => state.mail.hashMapMailMessages (state: RootState) => state.mail.hashMapMailMessages
) );
const fullMailMessages = useMemo(() => { const fullMailMessages = useMemo(() => {
return mailMessages.map((msg) => { return mailMessages.map(msg => {
let message = msg let message = msg;
const existingMessage = hashMapMailMessages[msg.id] const existingMessage = hashMapMailMessages[msg.id];
if (existingMessage) { if (existingMessage) {
message = existingMessage message = existingMessage;
} }
return message return message;
}) });
}, [mailMessages, hashMapMailMessages, user]) }, [mailMessages, hashMapMailMessages, user]);
const dispatch = useDispatch() const dispatch = useDispatch();
const navigate = useNavigate() const navigate = useNavigate();
const getAvatar = async (user: string) => { const getAvatar = async (user: string) => {
try { try {
let url = await qortalRequest({ let url = await qortalRequest({
action: 'GET_QDN_RESOURCE_URL', action: "GET_QDN_RESOURCE_URL",
name: user, name: user,
service: 'THUMBNAIL', service: "THUMBNAIL",
identifier: 'qortal_avatar' identifier: "qortal_avatar",
}) });
dispatch( dispatch(
setUserAvatarHash({ setUserAvatarHash({
name: user, name: user,
url url,
}) })
) );
} catch (error) {} } catch (error) {}
} };
const checkNewMessages = React.useCallback( const checkNewMessages = React.useCallback(
async (recipientName: string, recipientAddress: string) => { async (recipientName: string, recipientAddress: string) => {
try { try {
const query = `qortal_qmail_${value}_mail` const query = `qortal_qmail_${value}_mail`;
const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=50&includemetadata=true&reverse=true&excludeblocked=true` const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=50&includemetadata=true&reverse=true&excludeblocked=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
const latestPost = mailMessages[0] const latestPost = mailMessages[0];
if (!latestPost) return if (!latestPost) return;
const findPost = responseData?.findIndex( const findPost = responseData?.findIndex(
(item: any) => item?.identifier === latestPost?.id (item: any) => item?.identifier === latestPost?.id
) );
if (findPost === -1) { if (findPost === -1) {
return return;
} }
const newArray = responseData.slice(0, findPost) const newArray = responseData.slice(0, findPost);
const structureData = newArray.map((post: any): BlogPost => { const structureData = newArray.map((post: any): BlogPost => {
return { return {
title: post?.metadata?.title, title: post?.metadata?.title,
@ -120,49 +124,49 @@ export const AliasMail = ({ value, onOpen, messageOpenedId}: AliasMailProps) =>
createdAt: post?.created, createdAt: post?.created,
updated: post?.updated, updated: post?.updated,
user: post.name, user: post.name,
id: post.identifier id: post.identifier,
} };
}) });
setMailMessages((prev) => { setMailMessages(prev => {
const updatedMessages = [...prev] const updatedMessages = [...prev];
structureData.forEach((newMessage: any) => { structureData.forEach((newMessage: any) => {
const existingIndex = updatedMessages.findIndex( const existingIndex = updatedMessages.findIndex(
(prevMessage) => prevMessage.id === newMessage.id prevMessage => prevMessage.id === newMessage.id
) );
if (existingIndex !== -1) { if (existingIndex !== -1) {
// Replace existing message // Replace existing message
updatedMessages[existingIndex] = newMessage updatedMessages[existingIndex] = newMessage;
} else { } else {
// Add new message // Add new message
updatedMessages.unshift(newMessage) updatedMessages.unshift(newMessage);
} }
}) });
return updatedMessages return updatedMessages;
}) });
return return;
} catch (error) {} } catch (error) {}
}, },
[mailMessages, value] [mailMessages, value]
) );
const getMailMessages = React.useCallback( const getMailMessages = React.useCallback(
async (recipientName: string, recipientAddress: string) => { async (recipientName: string, recipientAddress: string) => {
try { try {
const offset = mailMessages.length const offset = mailMessages.length;
// dispatch(setIsLoadingGlobal(true)) // dispatch(setIsLoadingGlobal(true))
const query = `qortal_qmail_${value}_mail` const query = `qortal_qmail_${value}_mail`;
const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=50&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true` const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=50&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true`;
const response = await fetch(url, { const response = await fetch(url, {
method: 'GET', method: "GET",
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) });
const responseData = await response.json() const responseData = await response.json();
const structureData = responseData.map((post: any): BlogPost => { const structureData = responseData.map((post: any): BlogPost => {
return { return {
title: post?.metadata?.title, title: post?.metadata?.title,
@ -173,32 +177,32 @@ export const AliasMail = ({ value, onOpen, messageOpenedId}: AliasMailProps) =>
createdAt: post?.created, createdAt: post?.created,
updated: post?.updated, updated: post?.updated,
user: post.name, user: post.name,
id: post.identifier id: post.identifier,
} };
}) });
setMailMessages((prev) => { setMailMessages(prev => {
const updatedMessages = [...prev] const updatedMessages = [...prev];
structureData.forEach((newMessage: any) => { structureData.forEach((newMessage: any) => {
const existingIndex = updatedMessages.findIndex( const existingIndex = updatedMessages.findIndex(
(prevMessage) => prevMessage.id === newMessage.id prevMessage => prevMessage.id === newMessage.id
) );
if (existingIndex !== -1) { if (existingIndex !== -1) {
// Replace existing message // Replace existing message
updatedMessages[existingIndex] = newMessage updatedMessages[existingIndex] = newMessage;
} else { } else {
// Add new message // Add new message
updatedMessages.push(newMessage) updatedMessages.push(newMessage);
} }
}) });
return updatedMessages return updatedMessages;
}) });
for (const content of structureData) { for (const content of structureData) {
if (content.user && content.id) { if (content.user && content.id) {
getAvatar(content.user) getAvatar(content.user);
} }
} }
} catch (error) { } catch (error) {
@ -207,33 +211,33 @@ export const AliasMail = ({ value, onOpen, messageOpenedId}: AliasMailProps) =>
} }
}, },
[mailMessages, hashMapMailMessages, value] [mailMessages, hashMapMailMessages, value]
) );
const getMessages = React.useCallback(async () => { const getMessages = React.useCallback(async () => {
if (!user?.name || !user?.address) return if (!user?.name || !user?.address) return;
await getMailMessages(user.name, user.address) await getMailMessages(user.name, user.address);
}, [getMailMessages, user]) }, [getMailMessages, user]);
const interval = useRef<any>(null) const interval = useRef<any>(null);
const checkNewMessagesFunc = useCallback(() => { const checkNewMessagesFunc = useCallback(() => {
if (!user?.name || !user?.address) return if (!user?.name || !user?.address) return;
let isCalling = false let isCalling = false;
interval.current = setInterval(async () => { interval.current = setInterval(async () => {
if (isCalling || !user?.name || !user?.address) return if (isCalling || !user?.name || !user?.address) return;
isCalling = true isCalling = true;
const res = await checkNewMessages(user?.name, user.address) const res = await checkNewMessages(user?.name, user.address);
isCalling = false isCalling = false;
}, 30000) }, 30000);
}, [checkNewMessages, user]) }, [checkNewMessages, user]);
useEffect(() => { useEffect(() => {
checkNewMessagesFunc() checkNewMessagesFunc();
return () => { return () => {
if (interval?.current) { if (interval?.current) {
clearInterval(interval.current) clearInterval(interval.current);
} }
} };
}, [checkNewMessagesFunc]) }, [checkNewMessagesFunc]);
const openMessage = async ( const openMessage = async (
user: string, user: string,
@ -241,7 +245,7 @@ export const AliasMail = ({ value, onOpen, messageOpenedId}: AliasMailProps) =>
content: any content: any
) => { ) => {
try { try {
onOpen(user, messageIdentifier, {}) onOpen(user, messageIdentifier, {});
// const existingMessage: any = hashMapMailMessages[messageIdentifier] // const existingMessage: any = hashMapMailMessages[messageIdentifier]
// if (existingMessage && existingMessage.isValid && !existingMessage.unableToDecrypt) { // if (existingMessage && existingMessage.isValid && !existingMessage.unableToDecrypt) {
// setMessage(existingMessage) // setMessage(existingMessage)
@ -264,18 +268,18 @@ export const AliasMail = ({ value, onOpen, messageOpenedId}: AliasMailProps) =>
} catch (error) { } catch (error) {
} finally { } finally {
} }
} };
const firstMount = useRef<null | string>(null) const firstMount = useRef<null | string>(null);
useEffect(() => { useEffect(() => {
if (user?.name && (!firstMount.current || firstMount.current !== value)) { if (user?.name && (!firstMount.current || firstMount.current !== value)) {
setMailMessages([]) setMailMessages([]);
setTimeout(() => { setTimeout(() => {
getMessages() getMessages();
}, 100); }, 100);
firstMount.current = value firstMount.current = value;
} }
}, [user, value]) }, [user, value]);
return ( return (
<> <>
@ -294,26 +298,26 @@ export const AliasMail = ({ value, onOpen, messageOpenedId}: AliasMailProps) =>
setReplyTo={setReplyTo} setReplyTo={setReplyTo}
alias={value} alias={value}
/> />
<MessagesContainer> <MessagesContainer>
{fullMailMessages.map(item => { {fullMailMessages.map(item => {
return ( return (
<MailMessageRow <MailMessageRow
messageData={item} messageData={item}
openMessage={openMessage} openMessage={openMessage}
isOpen={messageOpenedId === item?.id} isOpen={messageOpenedId === item?.id}
/> />
); );
})} })}
<LazyLoad onLoadMore={getMessages}></LazyLoad> <LazyLoad onLoadMore={getMessages}></LazyLoad>
</MessagesContainer> </MessagesContainer>
{/* <SimpleTable {/* <SimpleTable
openMessage={openMessage} openMessage={openMessage}
data={fullMailMessages} data={fullMailMessages}
></SimpleTable> */} ></SimpleTable> */}
<Box <Box
sx={{ sx={{
width: '100%', width: "100%",
justifyContent: 'center' justifyContent: "center",
}} }}
> >
{mailMessages.length > 20 && ( {mailMessages.length > 20 && (
@ -321,9 +325,9 @@ export const AliasMail = ({ value, onOpen, messageOpenedId}: AliasMailProps) =>
)} )}
</Box> </Box>
{mailInfo && isShow && ( {mailInfo && isShow && (
<OpenMail open={isShow} handleClose={onOk} fileInfo={mailInfo}/> <OpenMail open={isShow} handleClose={onOk} fileInfo={mailInfo} />
)} )}
{/* <LazyLoad onLoadMore={getMessages}></LazyLoad> */} {/* <LazyLoad onLoadMore={getMessages}></LazyLoad> */}
</> </>
) );
} };

View File

@ -64,7 +64,7 @@ import {
ThreadSingleLastMessageP, ThreadSingleLastMessageP,
ThreadSingleLastMessageSpanP, ThreadSingleLastMessageSpanP,
ThreadSingleTitle, ThreadSingleTitle,
} from "./Mail-styles"; } from "../Home/Home-styles";
import { Spacer } from "../../components/common/Spacer"; import { Spacer } from "../../components/common/Spacer";
interface AliasMailProps { interface AliasMailProps {
groupInfo: any; groupInfo: any;

View File

@ -1,524 +0,0 @@
import { Box, Popover } from "@mui/material";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Step } from "react-joyride";
import { useSelector } from "react-redux";
import AddAliasSVG from "../../assets/svgs/AddAlias.svg";
import ArrowDownSVG from "../../assets/svgs/ArrowDown.svg";
import CheckSVG from "../../assets/svgs/Check.svg";
import { CloseSVG } from "../../assets/svgs/CloseSVG";
import ComposeIconSVG from "../../assets/svgs/ComposeIcon.svg";
import GroupSVG from "../../assets/svgs/Group.svg";
import SortSVG from "../../assets/svgs/Sort.svg";
import { useFetchMail } from "../../hooks/useFetchMail";
import { RootState } from "../../state/store";
import { executeEvent } from "../../utils/events";
import { GroupMail } from "./GroupMail";
import {
ArrowDownIcon,
CloseParent,
ComposeContainer,
ComposeContainerBlank,
ComposeIcon,
ComposeP,
InstanceContainer,
InstanceFooter,
InstanceLabel,
InstanceListContainer,
InstanceListContainerRow,
InstanceListContainerRowCheck,
InstanceListContainerRowCheckIcon,
InstanceListContainerRowGroupIcon,
InstanceListContainerRowLabelContainer,
InstanceListContainerRowMain,
InstanceListContainerRowMainP,
InstanceListHeader,
InstanceListParent,
InstanceP,
MailContainer,
SelectInstanceContainer,
SelectInstanceContainerFilterInner,
SelectInstanceContainerInner,
TypeInAliasTextfield,
} from "./Mail-styles";
const filterOptions = ["Recently active", "Newest", "Oldest"];
const steps: Step[] = [
{
content: (
<div>
<h2>Welcome To Q-Mail</h2>
<p
style={{
fontSize: "18px",
}}
>
Let's take a tour
</p>
<p
style={{
fontSize: "12px",
}}
>
The Qortal community, along with its development team and the creators
of this application, cannot be held accountable for any content
published or displayed. Furthermore, they bear no responsibility for
any data loss that may occur as a result of using this application.
</p>
</div>
),
placement: "center",
target: ".step-1",
},
{
target: ".step-3",
content: (
<div>
<h2>Changing instances</h2>
<p
style={{
fontSize: "18px",
fontFamily: "Arial",
}}
>
Toggle between your main inbox, alias' and private groups.
</p>
</div>
),
placement: "bottom",
},
{
target: ".step-2",
content: (
<div>
<h2>Composing a mail message</h2>
<p
style={{
fontSize: "18px",
fontWeight: "bold",
fontFamily: "Arial",
}}
>
Compose a secure message featuring encrypted attachments (up to 40MB
per attachment).
</p>
<p
style={{
fontSize: "18px",
fontFamily: "Arial",
}}
>
To protect the identity of the recipient, assign them an alias for
added anonymity.
</p>
</div>
),
placement: "bottom",
},
{
target: ".step-3",
content: (
<div>
<h2>What is an alias?</h2>
<p
style={{
fontSize: "18px",
fontWeight: "bold",
fontFamily: "Arial",
}}
>
To conceal the identity of the message recipient, utilize the alias
option when sending.
</p>
<p
style={{
fontSize: "14px",
fontFamily: "Arial",
}}
>
For instance, instruct your friend to address the message to you using
the alias 'FrederickGreat'.
</p>
<p
style={{
fontSize: "14px",
fontFamily: "Arial",
}}
>
To access messages sent to that alias, simply add the alias as an
instance.
</p>
</div>
),
placement: "bottom",
},
];
interface MailProps {
isFromTo: boolean;
}
export const Mail = ({ isFromTo }: MailProps) => {
const { user } = useSelector((state: RootState) => state.auth);
const [isOpenInstanceList, setIsOpenInstanceList] = useState<boolean>(false);
const [isOpenFilterList, setIsOpenFilterList] = useState<boolean>(false);
const anchorElInstance = useRef<any>(null);
const anchorElInstanceFilter = useRef<any>(null);
const [currentThread, setCurrentThread] = useState<any>(null);
const [aliasValue, setAliasValue] = useState("");
const [alias, setAlias] = useState<string[]>([]);
const [filterMode, setFilterMode] = useState<string>("Recently active");
const [selectedAlias, setSelectedAlias] = useState<string | null>(null);
const [selectedGroup, setSelectedGroup] = useState<any>(null);
const privateGroups = useSelector(
(state: RootState) => state.global.privateGroups
);
const options = useMemo(() => {
return Object.keys(privateGroups).map(key => {
return {
...privateGroups[key],
name: privateGroups[key].groupName,
id: key,
};
});
}, [privateGroups]);
const hashMapMailMessages = useSelector(
(state: RootState) => state.mail.hashMapMailMessages
);
const userName = useMemo(() => {
if (!user?.name) return "";
return user.name;
}, [user]);
const { getMailMessages, checkNewMessages } = useFetchMail();
const getMessages = React.useCallback(
async (isOnMount?: boolean) => {
if (!user?.name || !user?.address) return;
try {
await getMailMessages(user.name, user.address);
} catch (error) {}
},
[getMailMessages, user]
);
const interval = useRef<any>(null);
const checkNewMessagesFunc = useCallback(() => {
if (!user?.name || !user?.address) return;
let isCalling = false;
interval.current = setInterval(async () => {
if (isCalling || !user?.name || !user?.address) return;
isCalling = true;
const res = await checkNewMessages(user?.name, user.address);
isCalling = false;
}, 30000);
}, [checkNewMessages, user]);
useEffect(() => {
checkNewMessagesFunc();
return () => {
if (interval?.current) {
clearInterval(interval.current);
}
};
}, [checkNewMessagesFunc]);
const firstMount = useRef(false);
useEffect(() => {
if (user?.name && !firstMount.current) {
getMessages(true);
firstMount.current = true;
}
}, [user]);
useEffect(() => {
if (!userName) return;
const savedAlias = localStorage.getItem(`alias-qmail-${userName}`);
if (savedAlias) {
try {
setAlias(JSON.parse(savedAlias));
} catch (error) {
console.error("Error parsing JSON from localStorage:", error);
}
}
}, [userName]);
const handleCloseInstanceList = () => {
setIsOpenInstanceList(false);
};
const handleCloseThreadFilterList = () => {
setIsOpenFilterList(false);
};
const addAlias = () => {
if (!aliasValue) return;
const newList = [...alias, aliasValue];
if (userName) {
try {
localStorage.setItem(
`alias-qmail-${userName}`,
JSON.stringify(newList)
);
} catch (error) {}
}
setAlias(prev => [...prev, aliasValue]);
setAliasValue("");
};
const handleInputKeyDown = (event: any) => {
if (event.key === "Enter") {
addAlias();
}
};
const openNewThread = () => {
if (currentThread) {
executeEvent("openNewThreadMessageModal", {});
return;
}
executeEvent("openNewThreadModal", {});
};
useEffect(() => {
if (options[0] && !selectedGroup) setSelectedGroup(options[0]);
}, [options]);
return (
<MailContainer>
<InstanceContainer>
<ComposeContainer onClick={openNewThread}>
<ComposeIcon src={ComposeIconSVG} />
<ComposeP>{currentThread ? "New Post" : "New Thread"}</ComposeP>
</ComposeContainer>
<SelectInstanceContainer className="step-3">
<InstanceLabel>Current Instance:</InstanceLabel>
<SelectInstanceContainerInner
onClick={() => {
setIsOpenInstanceList(true);
}}
ref={anchorElInstance}
>
<InstanceP>
{selectedGroup ? selectedGroup?.name : user?.name}
</InstanceP>
<ArrowDownIcon src={ArrowDownSVG} />
</SelectInstanceContainerInner>
</SelectInstanceContainer>
<Popover
open={isOpenInstanceList}
anchorEl={anchorElInstance.current}
onClose={handleCloseInstanceList}
anchorOrigin={{
vertical: "bottom",
horizontal: "center",
}}
transformOrigin={{
vertical: "top",
horizontal: "center",
}}
style={{ marginTop: "8px" }} // Adjust this value as needed
>
<InstanceListParent>
<InstanceListHeader>
<InstanceListContainerRowLabelContainer>
<InstanceListContainerRowCheck></InstanceListContainerRowCheck>
<InstanceListContainerRowMain>
<InstanceListContainerRowMainP>
Instances:
</InstanceListContainerRowMainP>
</InstanceListContainerRowMain>
</InstanceListContainerRowLabelContainer>
</InstanceListHeader>
<InstanceListContainer>
{options?.map(group => {
return (
<InstanceListContainerRow
onClick={() => {
setSelectedAlias(null);
setSelectedGroup(group);
handleCloseInstanceList();
}}
sx={{
backgroundColor:
selectedGroup?.id === group?.id
? "rgba(74, 158, 244, 1)"
: "unset",
}}
key={group?.id}
>
<InstanceListContainerRowCheck>
{group?.id === selectedGroup?.id && (
<InstanceListContainerRowCheckIcon src={CheckSVG} />
)}
</InstanceListContainerRowCheck>
<InstanceListContainerRowMain>
<InstanceListContainerRowMainP>
{group?.name}
</InstanceListContainerRowMainP>
<CloseParent>
<InstanceListContainerRowGroupIcon src={GroupSVG} />
<Box
sx={{
visibility: "hidden",
}}
>
<CloseSVG
height=""
width=""
color="white"
opacity={0.2}
/>
</Box>
</CloseParent>
</InstanceListContainerRowMain>
</InstanceListContainerRow>
);
})}
</InstanceListContainer>
<InstanceFooter>
<InstanceListContainerRowLabelContainer>
<InstanceListContainerRowCheck>
<InstanceListContainerRowCheckIcon
onClick={addAlias}
src={AddAliasSVG}
sx={{
cursor: "pointer",
}}
/>
</InstanceListContainerRowCheck>
<TypeInAliasTextfield
onKeyDown={handleInputKeyDown}
value={aliasValue}
placeholder="Type in Alias"
onChange={e => {
setAliasValue(e.target.value);
}}
/>
</InstanceListContainerRowLabelContainer>
</InstanceFooter>
</InstanceListParent>
{/* <InstanceList /> */}
</Popover>
<Popover
open={isOpenFilterList}
anchorEl={anchorElInstanceFilter.current}
onClose={handleCloseThreadFilterList}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
>
<InstanceListParent
sx={{
minHeight: "unset",
width: "auto",
padding: "0px",
}}
>
<InstanceListHeader></InstanceListHeader>
<InstanceListContainer>
{filterOptions?.map(filter => {
return (
<InstanceListContainerRow
onClick={() => {
setFilterMode(filter);
handleCloseThreadFilterList();
}}
sx={{
backgroundColor:
filterMode === filter
? "rgba(74, 158, 244, 1)"
: "unset",
}}
key={filter}
>
<InstanceListContainerRowCheck>
{filter === filterMode && (
<InstanceListContainerRowCheckIcon src={CheckSVG} />
)}
</InstanceListContainerRowCheck>
<InstanceListContainerRowMain>
<InstanceListContainerRowMainP>
{filter}
</InstanceListContainerRowMainP>
</InstanceListContainerRowMain>
</InstanceListContainerRow>
);
})}
</InstanceListContainer>
<InstanceFooter></InstanceFooter>
</InstanceListParent>
</Popover>
<ComposeContainerBlank>
{selectedGroup && !currentThread && (
<ComposeContainer
onClick={() => {
setIsOpenFilterList(true);
}}
ref={anchorElInstanceFilter}
>
<ComposeIcon src={SortSVG} />
<SelectInstanceContainerFilterInner>
<ComposeP>Sort by</ComposeP>
<ArrowDownIcon src={ArrowDownSVG} />
</SelectInstanceContainerFilterInner>
</ComposeContainer>
)}
</ComposeContainerBlank>
</InstanceContainer>
<GroupMail
groupInfo={selectedGroup}
currentThread={currentThread}
setCurrentThread={setCurrentThread}
filterMode={filterMode}
setFilterMode={setFilterMode}
/>
</MailContainer>
);
};
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number | null;
}
export function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`mail-tabs-${index}`}
aria-labelledby={`mail-tabs-${index}`}
{...other}
style={{
width: "100%",
}}
>
{value === index && children}
</div>
);
}

View File

@ -9,11 +9,11 @@ import {
MessageExtraDate, MessageExtraDate,
MessageExtraInfo, MessageExtraInfo,
MessageExtraName, MessageExtraName,
} from "./Mail-styles"; } from "../Home/Home-styles";
import { AvatarWrapper } from "./MailTable"; import { AvatarWrapper } from "./MailTable";
import { formatTimestamp } from "../../utils/time"; import { formatTimestamp } from "../../utils/time";
import LockSVG from '../../assets/svgs/Lock.svg' import LockSVG from "../../assets/svgs/Lock.svg";
import AttachmentSVG from '../../assets/svgs/Attachment.svg' import AttachmentSVG from "../../assets/svgs/Attachment.svg";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { base64ToUint8Array, uint8ArrayToObject } from "../../utils/toBase64"; import { base64ToUint8Array, uint8ArrayToObject } from "../../utils/toBase64";
@ -24,9 +24,9 @@ function parseQuery(query: string) {
const match = query.match(regex); const match = query.match(regex);
if (match) { if (match) {
const recipientName = match[1]; const recipientName = match[1];
const recipientAddress = match[2] || null; // Will be null if the address part is not present const recipientAddress = match[2] || null; // Will be null if the address part is not present
return { recipientName, recipientAddress }; return { recipientName, recipientAddress };
} }
return { recipientName: null, recipientAddress: null }; return { recipientName: null, recipientAddress: null };
@ -35,141 +35,171 @@ interface MailMessageRowProp {
messageData: any; messageData: any;
openMessage: (user: string, id: string, content: any, alias?: string) => void; openMessage: (user: string, id: string, content: any, alias?: string) => void;
isOpen?: boolean; isOpen?: boolean;
isFromSent?: boolean isFromSent?: boolean;
} }
export const MailMessageRow = ({ export const MailMessageRow = ({
messageData, messageData,
openMessage, openMessage,
isOpen, isOpen,
isFromSent isFromSent,
}: MailMessageRowProp) => { }: MailMessageRowProp) => {
const username = useSelector((state: RootState) => state.auth?.user?.name) const username = useSelector((state: RootState) => state.auth?.user?.name);
const [subjectInHashDecrypted, setSubjectInHashDecrypted] = useState<null | string>(null) const [subjectInHashDecrypted, setSubjectInHashDecrypted] = useState<
const [hasAttachment, setHasAttachment] = useState<null | boolean>(null) null | string
>(null);
const [hasAttachment, setHasAttachment] = useState<null | boolean>(null);
const [sentToNameInfo, setSentToNameInfo] = useState({ const [sentToNameInfo, setSentToNameInfo] = useState({
name: "" name: "",
}) });
const [alias, setAlias] = useState<null | string>(null) const [alias, setAlias] = useState<null | string>(null);
const identifier = useMemo(()=> { const identifier = useMemo(() => {
return messageData?.id return messageData?.id;
}, [messageData]) }, [messageData]);
const getSentToName = useCallback(async (id: string)=> { const getSentToName = useCallback(async (id: string) => {
try { try {
const { recipientName, recipientAddress } = parseQuery(id); const { recipientName, recipientAddress } = parseQuery(id);
if(!recipientAddress && recipientName){ if (!recipientAddress && recipientName) {
setAlias(recipientName) setAlias(recipientName);
return return;
} }
if(!recipientName || !recipientAddress) return if (!recipientName || !recipientAddress) return;
const response = await qortalRequest({ const response = await qortalRequest({
action: "SEARCH_NAMES", action: "SEARCH_NAMES",
query: recipientName, query: recipientName,
prefix: true, prefix: true,
limit: 10, limit: 10,
reverse: false reverse: false,
}) });
const findName = response?.find((item: any)=> item?.owner?.includes(recipientAddress)) const findName = response?.find((item: any) =>
if(findName){ item?.owner?.includes(recipientAddress)
);
if (findName) {
setSentToNameInfo({ setSentToNameInfo({
name: findName.name name: findName.name,
}) });
} }
} catch (error) { } catch (error) {}
}, []);
useEffect(() => {
if (isFromSent && identifier) {
getSentToName(identifier);
} }
}, []) }, [isFromSent, identifier]);
useEffect(()=> { let isEncrypted = true;
if(isFromSent && identifier){ let hasAttachments = null;
getSentToName(identifier) let subject = "";
const hashMapMailMessages = useSelector(
(state: RootState) => state.mail.hashMapMailMessages
);
const hashMapSavedSubjects = useSelector(
(state: RootState) => state.mail.hashMapSavedSubjects
);
const subjectInHash = hashMapSavedSubjects[messageData?.id];
const data = hashMapMailMessages[messageData?.id];
if (subjectInHashDecrypted !== null) {
subject = subjectInHashDecrypted || "- no subject";
hasAttachments = hasAttachment || false;
}
if (data && data?.isValid && !data?.unableToDecrypt) {
isEncrypted = false;
subject = data?.subject || "- no subject";
hasAttachments = (data?.attachments || [])?.length > 0;
}
const getSubjectFromHash = async (
subject: string,
hasAttachmentParam: boolean
) => {
if (subject === undefined || subject === null)
throw new Error("no subject");
if (subject === "") {
setSubjectInHashDecrypted("");
setHasAttachment(hasAttachmentParam);
return;
} }
}, [isFromSent, identifier]) let requestEncryptSubject: any = {
let isEncrypted = true; action: "DECRYPT_DATA",
let hasAttachments = null encryptedData: subject,
let subject = "" };
const hashMapMailMessages = useSelector( const resDecryptSubject = await qortalRequest(requestEncryptSubject);
(state: RootState) => state.mail.hashMapMailMessages const decryptToUnit8ArraySubject = base64ToUint8Array(resDecryptSubject);
); const responseDataSubject = uint8ArrayToObject(decryptToUnit8ArraySubject);
const hashMapSavedSubjects = useSelector( if (responseDataSubject !== null || responseDataSubject !== undefined) {
(state: RootState) => state.mail.hashMapSavedSubjects setSubjectInHashDecrypted(responseDataSubject);
); setHasAttachment(hasAttachmentParam);
const subjectInHash = hashMapSavedSubjects[messageData?.id]
const data = hashMapMailMessages[messageData?.id]
if(subjectInHashDecrypted !== null){
subject = subjectInHashDecrypted || "- no subject"
hasAttachments = hasAttachment || false
}
if(data && data?.isValid && !data?.unableToDecrypt){
isEncrypted = false
subject = data?.subject || "- no subject"
hasAttachments = (data?.attachments || [])?.length > 0
}
const getSubjectFromHash = async (subject: string, hasAttachmentParam: boolean) => {
if(subject === undefined || subject === null) throw new Error('no subject')
if(subject === ""){
setSubjectInHashDecrypted("")
setHasAttachment(hasAttachmentParam)
return
}
let requestEncryptSubject: any = {
action: 'DECRYPT_DATA',
encryptedData: subject
}
const resDecryptSubject = await qortalRequest(requestEncryptSubject)
const decryptToUnit8ArraySubject =
base64ToUint8Array(resDecryptSubject)
const responseDataSubject = uint8ArrayToObject(
decryptToUnit8ArraySubject
)
if(responseDataSubject !== null || responseDataSubject !== undefined){
setSubjectInHashDecrypted(responseDataSubject)
setHasAttachment(hasAttachmentParam)
} }
} };
useEffect(() => {
if (!isEncrypted) return;
useEffect(()=> { if (subjectInHashDecrypted !== null) return;
if(!isEncrypted) return if (
!subjectInHash ||
subjectInHash?.subject === undefined ||
subjectInHash?.subject === null
)
return;
getSubjectFromHash(subjectInHash?.subject, subjectInHash?.attachments);
}, [isEncrypted, subjectInHashDecrypted]);
if(subjectInHashDecrypted !== null) return const name = useMemo(() => {
if(!subjectInHash || subjectInHash?.subject === undefined || subjectInHash?.subject === null ) return if (isFromSent) {
getSubjectFromHash(subjectInHash?.subject, subjectInHash?.attachments) return sentToNameInfo?.name || "";
}, [isEncrypted, subjectInHashDecrypted]) }
return messageData?.user;
}, [sentToNameInfo, isFromSent, messageData]);
const name = useMemo(()=> {
if(isFromSent){
return sentToNameInfo?.name || ""
}
return messageData?.user
}, [sentToNameInfo, isFromSent, messageData])
return ( return (
<MailMessageRowContainer sx={{ <MailMessageRowContainer
background: isOpen ? '#434448' : 'unset' sx={{
}} onClick={()=> { background: isOpen ? "#434448" : "unset",
openMessage(messageData?.user, messageData?.id, messageData, isFromSent ? (alias || name) : username) }}
}}> onClick={() => {
openMessage(
messageData?.user,
messageData?.id,
messageData,
isFromSent ? alias || name : username
);
}}
>
<MailMessageRowProfile> <MailMessageRowProfile>
<AvatarWrapper isAlias={!!alias} height="50px" user={name} fallback={alias || name}></AvatarWrapper> <AvatarWrapper
isAlias={!!alias}
height="50px"
user={name}
fallback={alias || name}
></AvatarWrapper>
<MessageExtraInfo> <MessageExtraInfo>
<MessageExtraName sx={{ <MessageExtraName
fontWeight: isFromSent ? '300' : isEncrypted ? '900' : '300' sx={{
}}>{isFromSent ? "To: " : ""} {alias || name}</MessageExtraName> fontWeight: isFromSent ? "300" : isEncrypted ? "900" : "300",
<MessageExtraDate>{formatTimestamp(messageData?.createdAt)}</MessageExtraDate> }}
>
{isFromSent ? "To: " : ""} {alias || name}
</MessageExtraName>
<MessageExtraDate>
{formatTimestamp(messageData?.createdAt)}
</MessageExtraDate>
</MessageExtraInfo> </MessageExtraInfo>
</MailMessageRowProfile> </MailMessageRowProfile>
<MailMessageRowInfo> <MailMessageRowInfo>
{hasAttachments ? <MailMessageRowInfoImg src={AttachmentSVG} /> : hasAttachments === false ? null : isEncrypted ? <MailMessageRowInfoImg src={LockSVG} /> : null} {hasAttachments ? (
<MailMessageRowInfoImg src={AttachmentSVG} />
) : hasAttachments === false ? null : isEncrypted ? (
<MailMessageRowInfoImg src={LockSVG} />
) : null}
{subject ? ( {subject ? (
<MailMessageRowInfoStatusRead>{subject}</MailMessageRowInfoStatusRead> <MailMessageRowInfoStatusRead>{subject}</MailMessageRowInfoStatusRead>
) : isEncrypted ? ( ) : isEncrypted ? (
<MailMessageRowInfoStatusNotDecrypted>ACCESS TO DECRYPT</MailMessageRowInfoStatusNotDecrypted> <MailMessageRowInfoStatusNotDecrypted>
ACCESS TO DECRYPT
</MailMessageRowInfoStatusNotDecrypted>
) : null} ) : null}
</MailMessageRowInfo> </MailMessageRowInfo>
</MailMessageRowContainer> </MailMessageRowContainer>

View File

@ -1,145 +1,143 @@
import * as React from 'react' import * as React from "react";
import Table from '@mui/material/Table' import Table from "@mui/material/Table";
import TableBody from '@mui/material/TableBody' import TableBody from "@mui/material/TableBody";
import TableCell from '@mui/material/TableCell' import TableCell from "@mui/material/TableCell";
import TableContainer from '@mui/material/TableContainer' import TableContainer from "@mui/material/TableContainer";
import TableHead from '@mui/material/TableHead' import TableHead from "@mui/material/TableHead";
import TableRow from '@mui/material/TableRow' import TableRow from "@mui/material/TableRow";
import Paper from '@mui/material/Paper' import Paper from "@mui/material/Paper";
import { Avatar, Box } from '@mui/material' import { Avatar, Box } from "@mui/material";
import { useSelector } from 'react-redux' import { useSelector } from "react-redux";
import { RootState } from '../../state/store' import { RootState } from "../../state/store";
import { formatTimestamp } from '../../utils/time' import { formatTimestamp } from "../../utils/time";
import AliasAvatar from '../../assets/svgs/AliasAvatar.svg' import AliasAvatar from "../../assets/svgs/AliasAvatar.svg";
import { AliasAvatarImg } from './Mail-styles' import { AliasAvatarImg } from "../Home/Home-styles";
const tableCellFontSize = '16px' const tableCellFontSize = "16px";
interface Data { interface Data {
name: string name: string;
description: string description: string;
createdAt: number createdAt: number;
user: string user: string;
id: string id: string;
tags: string[] tags: string[];
subject?: string subject?: string;
} }
interface ColumnData { interface ColumnData {
dataKey: keyof Data dataKey: keyof Data;
label: string label: string;
numeric?: boolean numeric?: boolean;
width?: number width?: number;
} }
const columns: ColumnData[] = [ const columns: ColumnData[] = [
{ {
label: 'Sender', label: "Sender",
dataKey: 'user', dataKey: "user",
width: 200 width: 200,
}, },
{ {
label: 'Subject', label: "Subject",
dataKey: 'description' dataKey: "description",
}, },
{ {
label: 'Date', label: "Date",
dataKey: 'createdAt', dataKey: "createdAt",
numeric: true, numeric: true,
width: 200 width: 200,
} },
] ];
function fixedHeaderContent() { function fixedHeaderContent() {
return ( return (
<TableRow> <TableRow>
{columns.map((column) => { {columns.map(column => {
return ( return (
<TableCell <TableCell
key={column.dataKey} key={column.dataKey}
variant="head" variant="head"
align={column.numeric || false ? 'right' : 'left'} align={column.numeric || false ? "right" : "left"}
style={{ width: column.width }} style={{ width: column.width }}
sx={{ sx={{
backgroundColor: 'background.paper', backgroundColor: "background.paper",
fontSize: tableCellFontSize, fontSize: tableCellFontSize,
padding: '7px' padding: "7px",
}} }}
> >
{column.label} {column.label}
</TableCell> </TableCell>
) );
})} })}
</TableRow> </TableRow>
) );
} }
function rowContent(_index: number, row: Data, openMessage: any) { function rowContent(_index: number, row: Data, openMessage: any) {
return ( return (
<React.Fragment> <React.Fragment>
{columns.map((column) => { {columns.map(column => {
let subject = '-' let subject = "-";
if (column.dataKey === 'description' && row['subject']) { if (column.dataKey === "description" && row["subject"]) {
subject = row['subject'] subject = row["subject"];
} }
return ( return (
<TableCell <TableCell
onClick={() => openMessage(row?.user, row?.id, row)} onClick={() => openMessage(row?.user, row?.id, row)}
key={column.dataKey} key={column.dataKey}
align={column.numeric || false ? 'right' : 'left'} align={column.numeric || false ? "right" : "left"}
style={{ width: column.width, cursor: 'pointer' }} style={{ width: column.width, cursor: "pointer" }}
sx={{ sx={{
fontSize: tableCellFontSize, fontSize: tableCellFontSize,
padding: '7px' padding: "7px",
}} }}
> >
{column.dataKey === 'user' && ( {column.dataKey === "user" && (
<Box <Box
sx={{ sx={{
display: 'flex', display: "flex",
gap: '5px', gap: "5px",
width: '100%', width: "100%",
alignItems: 'center', alignItems: "center",
flexWrap: 'wrap', flexWrap: "wrap",
textOverflow: 'ellipsis', textOverflow: "ellipsis",
overflow: 'hidden', overflow: "hidden",
whiteSpace: 'nowrap' whiteSpace: "nowrap",
}} }}
> >
<AvatarWrapper user={row?.user}></AvatarWrapper> <AvatarWrapper user={row?.user}></AvatarWrapper>
{row[column.dataKey]} {row[column.dataKey]}
</Box> </Box>
)} )}
{column.dataKey !== 'user' && ( {column.dataKey !== "user" && (
<> <>
{column.dataKey === 'createdAt' {column.dataKey === "createdAt"
? formatTimestamp(row[column.dataKey]) ? formatTimestamp(row[column.dataKey])
: column.dataKey === 'description' : column.dataKey === "description"
? subject ? subject
: row[column.dataKey]} : row[column.dataKey]}
</> </>
)} )}
</TableCell> </TableCell>
) );
})} })}
</React.Fragment> </React.Fragment>
) );
} }
interface SimpleTableProps { interface SimpleTableProps {
openMessage: (user: string, messageIdentifier: string, content: any) => void openMessage: (user: string, messageIdentifier: string, content: any) => void;
data: Data[] data: Data[];
children?: React.ReactNode children?: React.ReactNode;
} }
export default function SimpleTable({ export default function SimpleTable({
openMessage, openMessage,
data, data,
children children,
}: SimpleTableProps) { }: SimpleTableProps) {
return ( return (
<Paper style={{ width: '100%' }}> <Paper style={{ width: "100%" }}>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table> <Table>
<TableHead>{fixedHeaderContent()}</TableHead> <TableHead>{fixedHeaderContent()}</TableHead>
@ -154,24 +152,45 @@ export default function SimpleTable({
</TableContainer> </TableContainer>
{children} {children}
</Paper> </Paper>
) );
} }
export const AvatarWrapper = ({ user, height , fallback, isAlias}: any) => { export const AvatarWrapper = ({ user, height, fallback, isAlias }: any) => {
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
) );
const avatarLink = React.useMemo(() => { const avatarLink = React.useMemo(() => {
if (!user || !userAvatarHash) return '' if (!user || !userAvatarHash) return "";
const findUserAvatar = userAvatarHash[user] const findUserAvatar = userAvatarHash[user];
if (!findUserAvatar) return '' if (!findUserAvatar) return "";
return findUserAvatar return findUserAvatar;
}, [userAvatarHash, user]) }, [userAvatarHash, user]);
if(isAlias) return <AliasAvatarImg sx={{ if (isAlias)
width: height, return (
height: height <AliasAvatarImg
}} src={AliasAvatar}/> sx={{
if(!fallback) return <Avatar sx={{ width: height, height: height }} src={avatarLink} alt={user} /> width: height,
return <Avatar sx={{ width: height, height: height }} src={avatarLink} alt={fallback} >{fallback?.charAt(0)}</Avatar> height: height,
} }}
src={AliasAvatar}
/>
);
if (!fallback)
return (
<Avatar
sx={{ width: height, height: height }}
src={avatarLink}
alt={user}
/>
);
return (
<Avatar
sx={{ width: height, height: height }}
src={avatarLink}
alt={fallback}
>
{fallback?.charAt(0)}
</Avatar>
);
};

View File

@ -54,7 +54,7 @@ import {
NewMessageInputRow, NewMessageInputRow,
NewMessageSendButton, NewMessageSendButton,
NewMessageSendP, NewMessageSendP,
} from "./Mail-styles"; } from "../Home/Home-styles";
import ComposeIconSVG from "../../assets/svgs/ComposeIcon.svg"; import ComposeIconSVG from "../../assets/svgs/ComposeIcon.svg";
import ModalCloseSVG from "../../assets/svgs/ModalClose.svg"; import ModalCloseSVG from "../../assets/svgs/ModalClose.svg";
import AttachmentSVG from "../../assets/svgs/NewMessageAttachment.svg"; import AttachmentSVG from "../../assets/svgs/NewMessageAttachment.svg";

View File

@ -50,7 +50,7 @@ import {
NewMessageInputRow, NewMessageInputRow,
NewMessageSendButton, NewMessageSendButton,
NewMessageSendP, NewMessageSendP,
} from "./Mail-styles"; } from "../Home/Home-styles";
import { Spacer } from "../../components/common/Spacer"; import { Spacer } from "../../components/common/Spacer";
import { TextEditor } from "../../components/common/TextEditor/TextEditor"; import { TextEditor } from "../../components/common/TextEditor/TextEditor";
import { SendNewMessage } from "../../assets/svgs/SendNewMessage"; import { SendNewMessage } from "../../assets/svgs/SendNewMessage";

View File

@ -38,7 +38,7 @@ import { BlogPost } from "../../state/features/blogSlice";
import { setNotification } from "../../state/features/notificationsSlice"; import { setNotification } from "../../state/features/notificationsSlice";
import { useModal } from "../../components/common/useModal"; import { useModal } from "../../components/common/useModal";
import { OpenMail } from "./OpenMail"; import { OpenMail } from "./OpenMail";
import { MessagesContainer } from "./Mail-styles"; import { MessagesContainer } from "../Home/Home-styles";
import { MailMessageRow } from "./MailMessageRow"; import { MailMessageRow } from "./MailMessageRow";
interface SentMailProps { interface SentMailProps {

View File

@ -35,7 +35,7 @@ import {
ShowMessageNameP, ShowMessageNameP,
ShowMessageSubjectP, ShowMessageSubjectP,
ShowMessageTimeP, ShowMessageTimeP,
} from "./Mail-styles"; } from "../Home/Home-styles";
import ReplySVG from "../../assets/svgs/Reply.svg"; import ReplySVG from "../../assets/svgs/Reply.svg";
import ForwardSVG from "../../assets/svgs/Forward.svg"; import ForwardSVG from "../../assets/svgs/Forward.svg";
import AttachmentMailSVG from "../../assets/svgs/AttachmentMail.svg"; import AttachmentMailSVG from "../../assets/svgs/AttachmentMail.svg";
@ -57,14 +57,14 @@ export const ShowMessageV2 = ({
message, message,
setReplyTo, setReplyTo,
alias, alias,
setForwardInfo setForwardInfo,
}: any) => { }: any) => {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [attachments, setAttachments] = useState<any[]>([]); const [attachments, setAttachments] = useState<any[]>([]);
const [description, setDescription] = useState<string>(""); const [description, setDescription] = useState<string>("");
const [expandAttachments, setExpandAttachments] = useState<boolean>(false); const [expandAttachments, setExpandAttachments] = useState<boolean>(false);
const [toUser, setToUser] = useState(null) const [toUser, setToUser] = useState(null);
const [destinationName, setDestinationName] = useState(""); const [destinationName, setDestinationName] = useState("");
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -79,33 +79,33 @@ export const ShowMessageV2 = ({
setReplyTo(message); setReplyTo(message);
}; };
const handleForwardedMessage = () => {
if (!username) return;
let secondpart = "";
const handleForwardedMessage = ()=> { if (message?.textContentV2) {
if(!username) return secondpart = message.textContentV2;
let secondpart = ""
if(message?.textContentV2){
secondpart = message.textContentV2
} }
if (message?.htmlContent) { if (message?.htmlContent) {
secondpart = DOMPurify.sanitize(message.htmlContent); secondpart = DOMPurify.sanitize(message.htmlContent);
} }
let newTo = username let newTo = username;
if(alias){ if (alias) {
newTo = `${alias} (alias)` newTo = `${alias} (alias)`;
} }
const firstPart = updateMessageDetails(message?.user, message?.subject || "", newTo) const firstPart = updateMessageDetails(
const fullMessage = firstPart + secondpart message?.user,
setForwardInfo(fullMessage) message?.subject || "",
} newTo
);
const fullMessage = firstPart + secondpart;
setForwardInfo(fullMessage);
};
let cleanHTML = ""; let cleanHTML = "";
if (message?.htmlContent) { if (message?.htmlContent) {
cleanHTML = DOMPurify.sanitize(message.htmlContent); cleanHTML = DOMPurify.sanitize(message.htmlContent);
} }
return ( return (
<Box <Box
sx={{ sx={{
@ -154,9 +154,11 @@ export const ShowMessageV2 = ({
}} }}
> >
<ShowMessageNameP>{message?.user}</ShowMessageNameP> <ShowMessageNameP>{message?.user}</ShowMessageNameP>
<ShowMessageTimeP sx={{ <ShowMessageTimeP
marginTop: '2px' sx={{
}}> marginTop: "2px",
}}
>
to: {message?.to} to: {message?.to}
</ShowMessageTimeP> </ShowMessageTimeP>
<ShowMessageTimeP> <ShowMessageTimeP>
@ -181,78 +183,81 @@ export const ShowMessageV2 = ({
marginTop: "10px", marginTop: "10px",
}} }}
> >
{message?.attachments {message?.attachments.map((file: any, index: number) => {
.map((file: any, index: number) => { const isFirst = index === 0;
const isFirst = index === 0 return (
return ( <Box
sx={{
display: expandAttachments
? "flex"
: !expandAttachments && isFirst
? "flex"
: "none",
alignItems: "center",
justifyContent: "flex-start",
width: "100%",
}}
>
<Box <Box
sx={{ sx={{
display: expandAttachments ? "flex" : !expandAttachments && isFirst ? 'flex' : 'none', display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "flex-start", gap: "5px",
width: "100%", cursor: "pointer",
width: "auto",
}} }}
> >
<Box <FileElement
sx={{ fileInfo={{ ...file, mimeTypeSaved: file?.type }}
display: "flex", title={file?.filename}
alignItems: "center", mode="mail"
gap: "5px", otherUser={message?.user}
cursor: "pointer",
width: "auto",
}}
> >
<FileElement <MailAttachmentImg src={AttachmentMailSVG} />
fileInfo={{ ...file, mimeTypeSaved: file?.type }}
title={file?.filename}
mode="mail"
otherUser={message?.user}
>
<MailAttachmentImg src={AttachmentMailSVG} />
<Typography <Typography
sx={{
fontSize: "16px",
transition: "0.2s all",
"&:hover": {
textDecoration: "underline",
},
}}
>
{file?.originalFilename || file?.filename}
</Typography>
</FileElement>
{message?.attachments?.length > 1 && isFirst && (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
}}
onClick={() => {
setExpandAttachments(prev => !prev);
}}
>
<MoreImg
sx={{ sx={{
fontSize: "16px", marginLeft: "5px",
transition: '0.2s all', transform: expandAttachments
"&:hover": { ? "rotate(180deg)"
textDecoration: 'underline' : "unset",
}
}} }}
> src={MoreSVG}
{file?.originalFilename || file?.filename} />
</Typography> <MoreP>
</FileElement> {expandAttachments
{message?.attachments?.length > 1 && isFirst && ( ? "hide"
<Box : `(${message?.attachments?.length - 1} more)`}
sx={{ </MoreP>
display: "flex", </Box>
alignItems: "center", )}
gap: "5px",
}}
onClick={() => {
setExpandAttachments(prev => !prev);
}}
>
<MoreImg
sx={{
marginLeft: "5px",
transform: expandAttachments
? "rotate(180deg)"
: "unset",
}}
src={MoreSVG}
/>
<MoreP>
{expandAttachments ? 'hide' : `(${message?.attachments?.length - 1} more)`}
</MoreP>
</Box>
)}
</Box>
</Box> </Box>
); </Box>
}) );
} })}
</Box> </Box>
)} )}
<Spacer height="7px" /> <Spacer height="7px" />
@ -268,7 +273,7 @@ export const ShowMessageV2 = ({
display: "flex", display: "flex",
gap: "25px", gap: "25px",
justifyContent: "center", justifyContent: "center",
marginTop: '10px' marginTop: "10px",
}} }}
> >
<ShowMessageButton onClick={handleReply}> <ShowMessageButton onClick={handleReply}>
@ -292,31 +297,33 @@ export const ShowMessageV2 = ({
gap: "8px", gap: "8px",
}} }}
> >
{message?.generalData?.threadV2 && [...(message?.generalData?.threadV2 || [])] // Create a copy of the array {message?.generalData?.threadV2 &&
.sort((a, b) => { [...(message?.generalData?.threadV2 || [])] // Create a copy of the array
// Sort messages based on createdAt in descending order .sort((a, b) => {
if (!a.data || !b.data) return 0; // Sort messages based on createdAt in descending order
return a.data.createdAt - b.data.createdAt; if (!a.data || !b.data) return 0;
}) return a.data.createdAt - b.data.createdAt;
.map(msg => { })
// Render each message using a component .map(msg => {
if (!msg?.data) return null; // Render each message using a component
return <ShowMessageV2Replies key={msg.data.id} message={msg.data} />; if (!msg?.data) return null;
}) return (
} <ShowMessageV2Replies key={msg.data.id} message={msg.data} />
);
})}
{message?.htmlContent && ( {message?.htmlContent && (
<div dangerouslySetInnerHTML={{ __html: cleanHTML }} /> <div dangerouslySetInnerHTML={{ __html: cleanHTML }} />
)} )}
<Box sx={{ <Box
width: '100%', sx={{
padding: '15px' width: "100%",
}}> padding: "15px",
}}
>
{message?.textContent && ( {message?.textContent && (
<ReadOnlySlate content={message.textContent} mode="mail" /> <ReadOnlySlate content={message.textContent} mode="mail" />
)} )}
</Box> </Box>
</Box> </Box>
</Box> </Box>
); );

View File

@ -25,13 +25,22 @@ import FileElement from "../../components/FileElement";
import MailThreadWithoutCalling from "./MailThreadWithoutCalling"; import MailThreadWithoutCalling from "./MailThreadWithoutCalling";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml"; import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import { Spacer } from "../../components/common/Spacer"; import { Spacer } from "../../components/common/Spacer";
import { MailAttachmentImg, MoreImg, MoreP, ShowMessageButton, ShowMessageButtonImg, ShowMessageButtonP, ShowMessageNameP, ShowMessageSubjectP, ShowMessageTimeP } from "./Mail-styles"; import {
import ReplySVG from '../../assets/svgs/Reply.svg' MailAttachmentImg,
import ForwardSVG from '../../assets/svgs/Forward.svg' MoreImg,
MoreP,
ShowMessageButton,
ShowMessageButtonImg,
ShowMessageButtonP,
ShowMessageNameP,
ShowMessageSubjectP,
ShowMessageTimeP,
} from "../Home/Home-styles";
import ReplySVG from "../../assets/svgs/Reply.svg";
import ForwardSVG from "../../assets/svgs/Forward.svg";
import MoreSVG from "../../assets/svgs/More.svg"; import MoreSVG from "../../assets/svgs/More.svg";
import AttachmentMailSVG from "../../assets/svgs/AttachmentMail.svg"; import AttachmentMailSVG from "../../assets/svgs/AttachmentMail.svg";
const initialValue: Descendant[] = [ const initialValue: Descendant[] = [
{ {
type: "paragraph", type: "paragraph",
@ -40,21 +49,15 @@ const initialValue: Descendant[] = [
]; ];
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
export const ShowMessageV2Replies = ({ export const ShowMessageV2Replies = ({ message }: any) => {
message,
}: any) => {
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const [expandAttachments, setExpandAttachments] = useState<boolean>(false); const [expandAttachments, setExpandAttachments] = useState<boolean>(false);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const isUser = useMemo(() => {
const isUser = useMemo(()=> { return username === message?.user;
return username === message?.user }, [username, message]);
}, [username, message])
let cleanHTML = ""; let cleanHTML = "";
if (message?.htmlContent) { if (message?.htmlContent) {
@ -62,199 +65,203 @@ export const ShowMessageV2Replies = ({
} }
return ( return (
<Box sx={{
padding: !isUser ? '0px 15px' : 'unset',
flexShrink: 0,
width: '100%'
}}
className={isUser ? "" : "reply-other"}
>
<Box <Box
sx={{ sx={{
display: "flex", padding: !isUser ? "0px 15px" : "unset",
flexDirection: "column", flexShrink: 0,
width: "100%", width: "100%",
'border-radius': '4px',
'background': !isUser ? 'rgba(255, 255, 255, 0.80)' : 'unset',
padding: '7px',
}} }}
className={isUser ? "" : "reply-other"}
> >
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center",
flexDirection: "column", flexDirection: "column",
gap: 1,
flexGrow: 1,
overflow: "auto",
width: "100%", width: "100%",
padding: "0 15px", "border-radius": "4px",
background: !isUser ? "rgba(255, 255, 255, 0.80)" : "unset",
padding: "7px",
}} }}
> >
<Box <Box
onClick={()=> {
setIsExpanded((prev)=> !prev)
}}
sx={{ sx={{
cursor: 'pointer',
display: "flex", display: "flex",
gap: '20px', alignItems: "center",
justifyContent: "flex-start", flexDirection: "column",
alignItems: "flex-start", gap: 1,
flexGrow: 1,
overflow: "auto",
width: "100%", width: "100%",
flexDirection: !isUser ? 'row-reverse' : 'unset' padding: "0 15px",
}} }}
> >
<Box <Box
onClick={() => {
setIsExpanded(prev => !prev);
}}
sx={{ sx={{
cursor: "pointer",
display: "flex", display: "flex",
gap: "20px",
justifyContent: "flex-start",
alignItems: "flex-start", alignItems: "flex-start",
gap: "6px" width: "100%",
flexDirection: !isUser ? "row-reverse" : "unset",
}} }}
> >
<AvatarWrapper height="40px" user={message?.user} /> <Box
<Box sx={{ sx={{
display: "flex", display: "flex",
flexDirection: 'column', alignItems: "flex-start",
gap: '1px', gap: "6px",
justifyContent: "flex-start", }}
maxWidth: '160px',
minWidth: '120px'
}}>
<ShowMessageNameP
sx={{
color: !isUser ? 'black' : 'unset'
}}
> >
{message?.user} <AvatarWrapper height="40px" user={message?.user} />
</ShowMessageNameP> <Box
<ShowMessageTimeP sx={{
sx={{ display: "flex",
color: !isUser ? 'black' : 'unset' flexDirection: "column",
}} gap: "1px",
> justifyContent: "flex-start",
{formatEmailDate(message?.createdAt)} maxWidth: "160px",
</ShowMessageTimeP> minWidth: "120px",
}}
>
<ShowMessageNameP
sx={{
color: !isUser ? "black" : "unset",
}}
>
{message?.user}
</ShowMessageNameP>
<ShowMessageTimeP
sx={{
color: !isUser ? "black" : "unset",
}}
>
{formatEmailDate(message?.createdAt)}
</ShowMessageTimeP>
</Box>
</Box> </Box>
<Box
</Box> sx={{
<Box display: "flex",
sx={{ alignItems: "flex-start",
display: "flex", gap: "10px",
alignItems: "flex-start", }}
gap: "10px",
}}
>
<ShowMessageSubjectP
sx={{
color: !isUser ? 'black' : 'unset'
}}
> >
{message?.subject} <ShowMessageSubjectP
</ShowMessageSubjectP> sx={{
color: !isUser ? "black" : "unset",
}}
>
{message?.subject}
</ShowMessageSubjectP>
</Box>
</Box> </Box>
</Box> {isExpanded && (
{isExpanded && ( <>
<> {message?.attachments?.length > 0 && (
{message?.attachments?.length > 0 && ( <Box
<Box sx={{
sx={{ width: "100%",
width: "100%", marginTop: "10px",
marginTop: "10px", }}
}} >
> {message?.attachments?.length > 0 && (
{message?.attachments?.length > 0 && (
<Box
sx={{
width: "100%",
marginTop: "10px",
}}
>
{message?.attachments
.map((file: any, index: number) => {
const isFirst = index === 0
return (
<Box
sx={{
display: expandAttachments ? "flex" : !expandAttachments && isFirst ? 'flex' : 'none',
alignItems: "center",
justifyContent: "flex-start",
width: "100%",
}}
>
<Box <Box
sx={{ sx={{
display: "flex", width: "100%",
alignItems: "center", marginTop: "10px",
gap: "5px",
cursor: "pointer",
width: "auto",
}} }}
> >
<FileElement {message?.attachments.map((file: any, index: number) => {
fileInfo={{ ...file, mimeTypeSaved: file?.type }} const isFirst = index === 0;
title={file?.filename} return (
mode="mail" <Box
otherUser={message?.user}
>
<MailAttachmentImg src={AttachmentMailSVG} />
<Typography
sx={{
fontSize: "16px",
}}
>
{file?.originalFilename || file?.filename}
</Typography>
</FileElement>
{message?.attachments?.length > 1 && isFirst && (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
}}
onClick={() => {
setExpandAttachments(prev => !prev);
}}
>
<MoreImg
sx={{ sx={{
marginLeft: "5px", display: expandAttachments
transform: expandAttachments ? "flex"
? "rotate(180deg)" : !expandAttachments && isFirst
: "unset", ? "flex"
: "none",
alignItems: "center",
justifyContent: "flex-start",
width: "100%",
}} }}
src={MoreSVG} >
/> <Box
<MoreP> sx={{
({message?.attachments?.length - 1} more) display: "flex",
</MoreP> alignItems: "center",
</Box> gap: "5px",
)} cursor: "pointer",
width: "auto",
}}
>
<FileElement
fileInfo={{
...file,
mimeTypeSaved: file?.type,
}}
title={file?.filename}
mode="mail"
otherUser={message?.user}
>
<MailAttachmentImg src={AttachmentMailSVG} />
<Typography
sx={{
fontSize: "16px",
}}
>
{file?.originalFilename || file?.filename}
</Typography>
</FileElement>
{message?.attachments?.length > 1 && isFirst && (
<Box
sx={{
display: "flex",
alignItems: "center",
gap: "5px",
}}
onClick={() => {
setExpandAttachments(prev => !prev);
}}
>
<MoreImg
sx={{
marginLeft: "5px",
transform: expandAttachments
? "rotate(180deg)"
: "unset",
}}
src={MoreSVG}
/>
<MoreP>
({message?.attachments?.length - 1} more)
</MoreP>
</Box>
)}
</Box>
</Box>
);
})}
</Box> </Box>
</Box> )}
); </Box>
}) )}
} <Spacer height="7px" />
</Box> {message?.textContentV2 && (
)} <DisplayHtml
</Box> html={message?.textContentV2}
)} textColor={!isUser ? "black" : "white"}
<Spacer height="7px" /> />
{message?.textContentV2 && ( )}
<DisplayHtml html={message?.textContentV2} textColor={!isUser ? "black": "white"}/> </>
)} )}
</> </Box>
)}
</Box> </Box>
</Box>
</Box> </Box>
); );
}; };

View File

@ -38,7 +38,7 @@ import {
ThreadSingleLastMessageP, ThreadSingleLastMessageP,
ThreadSingleLastMessageSpanP, ThreadSingleLastMessageSpanP,
ThreadSingleTitle, ThreadSingleTitle,
} from "./Mail-styles"; } from "../Home/Home-styles";
import { Spacer } from "../../components/common/Spacer"; import { Spacer } from "../../components/common/Spacer";
const initialValue: Descendant[] = [ const initialValue: Descendant[] = [
{ {

View File

@ -37,7 +37,7 @@ import {
SingleThreadParent, SingleThreadParent,
ThreadContainer, ThreadContainer,
ThreadContainerFullWidth, ThreadContainerFullWidth,
} from "./Mail-styles"; } from "../Home/Home-styles";
import { Spacer } from "../../components/common/Spacer"; import { Spacer } from "../../components/common/Spacer";
import ReturnSVG from "../../assets/svgs/Return.svg"; import ReturnSVG from "../../assets/svgs/Return.svg";
import LazyLoad from "../../components/common/LazyLoad"; import LazyLoad from "../../components/common/LazyLoad";