diff --git a/src/App.tsx b/src/App.tsx index 815f3ef..f0f264f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,6 @@ import GlobalWrapper from './wrappers/GlobalWrapper' import DownloadWrapper from './wrappers/DownloadWrapper' import Notification from './components/common/Notification/Notification' import { useState } from 'react' -import { Mail } from './pages/Mail/Mail' function App() { const themeColor = window._qdnTheme @@ -54,7 +53,6 @@ function App() { path="/subscriptions" element={<BlogList mode="subscriptions" />} /> - <Route path="/mail" element={<Mail />} /> <Route path="/" element={<BlogList />} /> </Routes> </GlobalWrapper> diff --git a/src/hooks/useFetchMail.tsx b/src/hooks/useFetchMail.tsx deleted file mode 100644 index b156ee0..0000000 --- a/src/hooks/useFetchMail.tsx +++ /dev/null @@ -1,469 +0,0 @@ -import React from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { - addPosts, - addToHashMap, - BlogPost, - populateFavorites, - setCountNewPosts, - upsertFilteredPosts, - upsertPosts, - upsertPostsBeginning, - upsertSubscriptionPosts -} from '../state/features/blogSlice' -import { - setCurrentBlog, - setIsLoadingGlobal, - setUserAvatarHash -} from '../state/features/globalSlice' -import { RootState } from '../state/store' -import { fetchAndEvaluatePosts } from '../utils/fetchPosts' -import { fetchAndEvaluateMail } from '../utils/fetchMail' -import { - addToHashMapMail, - upsertMessages, - upsertMessagesBeginning -} from '../state/features/mailSlice' -import { MAIL_SERVICE_TYPE } from '../constants/mail' - -export const useFetchMail = () => { - const dispatch = useDispatch() - const hashMapPosts = useSelector( - (state: RootState) => state.blog.hashMapPosts - ) - const hashMapMailMessages = useSelector( - (state: RootState) => state.mail.hashMapMailMessages - ) - const posts = useSelector((state: RootState) => state.blog.posts) - const mailMessages = useSelector( - (state: RootState) => state.mail.mailMessages - ) - - const filteredPosts = useSelector( - (state: RootState) => state.blog.filteredPosts - ) - const favoritesLocal = useSelector( - (state: RootState) => state.blog.favoritesLocal - ) - const favorites = useSelector((state: RootState) => state.blog.favorites) - const subscriptionPosts = useSelector( - (state: RootState) => state.blog.subscriptionPosts - ) - const subscriptions = useSelector( - (state: RootState) => state.blog.subscriptions - ) - - const checkAndUpdatePost = React.useCallback( - (post: BlogPost) => { - // Check if the post exists in hashMapPosts - const existingPost = hashMapPosts[post.id] - if (!existingPost) { - // If the post doesn't exist, add it to hashMapPosts - return true - } else if ( - post?.updated && - existingPost?.updated && - (!existingPost?.updated || post?.updated) > existingPost?.updated - ) { - // If the post exists and its updated is more recent than the existing post's updated, update it in hashMapPosts - return true - } else { - return false - } - }, - [hashMapPosts] - ) - - const getBlogPost = async (user: string, postId: string, content: any) => { - const res = await fetchAndEvaluatePosts({ - user, - postId, - content - }) - - dispatch(addToHashMap(res)) - } - - const getMailMessage = async (user: string, postId: string, content: any) => { - const res = await fetchAndEvaluateMail({ - user, - postId, - content - }) - - dispatch(addToHashMapMail(res)) - } - - const checkNewMessages = React.useCallback( - async (recipientName: string, recipientAddress: string) => { - try { - const query = `qortal_qmail_${recipientName.slice( - 0, - 20 - )}_${recipientAddress.slice(-6)}_mail_` - const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=20&includemetadata=true&reverse=true&excludeblocked=true` - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const latestPost = mailMessages[0] - if (!latestPost) return - const findPost = responseData?.findIndex( - (item: any) => item?.identifier === latestPost?.id - ) - if (findPost === -1) { - return - } - const newArray = responseData.slice(0, findPost) - const structureData = newArray.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: post?.created, - updated: post?.updated, - user: post.name, - id: post.identifier - } - }) - dispatch(upsertMessagesBeginning(structureData)) - return - } catch (error) {} - }, - [mailMessages] - ) - - const getNewPosts = React.useCallback(async () => { - try { - dispatch(setIsLoadingGlobal(true)) - dispatch(setCountNewPosts(0)) - const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&reverse=true&excludeblocked=true` - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const latestPost = posts[0] - if (!latestPost) return - const findPost = responseData?.findIndex( - (item: any) => item?.identifier === latestPost?.id - ) - let fetchAll = responseData - let willFetchAll = true - if (findPost !== -1) { - willFetchAll = false - fetchAll = responseData.slice(0, findPost) - } - - const structureData = fetchAll.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: post?.created, - updated: post?.updated, - user: post.name, - postImage: '', - id: post.identifier - } - }) - if (!willFetchAll) { - dispatch(upsertPostsBeginning(structureData)) - } - if (willFetchAll) { - dispatch(addPosts(structureData)) - } - - for (const content of structureData) { - if (content.user && content.id) { - const res = checkAndUpdatePost(content) - if (res) { - getBlogPost(content.user, content.id, content) - } - } - } - } catch (error) { - } finally { - dispatch(setIsLoadingGlobal(false)) - } - }, [posts, hashMapPosts]) - - const getBlogPosts = React.useCallback(async () => { - try { - const offset = posts.length - - dispatch(setIsLoadingGlobal(true)) - const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true` - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const structureData = responseData.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: post?.created, - updated: post?.updated, - user: post.name, - postImage: '', - id: post.identifier - } - }) - dispatch(upsertPosts(structureData)) - - for (const content of structureData) { - if (content.user && content.id) { - const res = checkAndUpdatePost(content) - if (res) { - getBlogPost(content.user, content.id, content) - } - } - } - } catch (error) { - } finally { - dispatch(setIsLoadingGlobal(false)) - } - }, [posts, hashMapPosts]) - - const getAvatar = async (user: string) => { - try { - let url = await qortalRequest({ - action: 'GET_QDN_RESOURCE_URL', - name: user, - service: 'THUMBNAIL', - identifier: 'qortal_avatar' - }) - dispatch( - setUserAvatarHash({ - name: user, - url - }) - ) - } catch (error) {} - } - const getMailMessages = React.useCallback( - async (recipientName: string, recipientAddress: string) => { - try { - const offset = mailMessages.length - - dispatch(setIsLoadingGlobal(true)) - const query = `qortal_qmail_${recipientName.slice( - 0, - 20 - )}_${recipientAddress.slice(-6)}_mail_` - 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', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const structureData = responseData.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: post?.created, - updated: post?.updated, - user: post.name, - id: post.identifier - } - }) - dispatch(upsertMessages(structureData)) - - for (const content of structureData) { - if (content.user && content.id) { - getAvatar(content.user) - } - } - } catch (error) { - } finally { - dispatch(setIsLoadingGlobal(false)) - } - }, - [mailMessages, hashMapMailMessages] - ) - const getBlogFilteredPosts = React.useCallback( - async (filterValue: string) => { - try { - const offset = filteredPosts.length - - dispatch(setIsLoadingGlobal(true)) - const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true&name=${filterValue}` - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const structureData = responseData.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: post?.created, - updated: post?.updated, - user: post.name, - postImage: '', - id: post.identifier - } - }) - dispatch(upsertFilteredPosts(structureData)) - - for (const content of structureData) { - if (content.user && content.id) { - const res = checkAndUpdatePost(content) - if (res) { - getBlogPost(content.user, content.id, content) - } - } - } - } catch (error) { - } finally { - dispatch(setIsLoadingGlobal(false)) - } - }, - [filteredPosts, hashMapPosts] - ) - - const getBlogPostsSubscriptions = React.useCallback( - async (username: string) => { - try { - const offset = subscriptionPosts.length - dispatch(setIsLoadingGlobal(true)) - const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&namefilter=q-blog-subscriptions-${username}&excludeblocked=true` - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const structureData = responseData.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: '', - user: post.name, - postImage: '', - id: post.identifier - } - }) - dispatch(upsertSubscriptionPosts(structureData)) - - for (const content of structureData) { - if (content.user && content.id) { - const res = checkAndUpdatePost(content) - if (res) { - getBlogPost(content.user, content.id, content) - } - } - } - } catch (error) { - } finally { - dispatch(setIsLoadingGlobal(false)) - } - }, - [subscriptionPosts, hashMapPosts, subscriptions] - ) - - const getBlogPostsFavorites = React.useCallback(async () => { - try { - const offset = favorites.length - const favSlice = (favoritesLocal || []).slice(offset, 20) - let favs = [] - for (const item of favSlice) { - try { - // await qortalRequest({ - // action: "SEARCH_QDN_RESOURCES", - // service: "THUMBNAIL", - // query: "search query goes here", // Optional - searches both "identifier" and "name" fields - // identifier: "search query goes here", // Optional - searches only the "identifier" field - // name: "search query goes here", // Optional - searches only the "name" field - // prefix: false, // Optional - if true, only the beginning of fields are matched in all of the above filters - // default: false, // Optional - if true, only resources without identifiers are returned - // includeStatus: false, // Optional - will take time to respond, so only request if necessary - // includeMetadata: false, // Optional - will take time to respond, so only request if necessary - // limit: 100, - // offset: 0, - // reverse: true - // }); - //TODO - NAME SHOULD BE EXACT - const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&identifier=${item.id}&exactmatchnames=true&name=${item.user}&limit=20&includemetadata=true&reverse=true&excludeblocked=true` - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - const data = await response.json() - // - if (data.length > 0) { - favs.push(data[0]) - } - } catch (error) {} - } - const structureData = favs.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: '', - user: post.name, - postImage: '', - id: post.identifier - } - }) - dispatch(populateFavorites(structureData)) - - for (const content of structureData) { - if (content.user && content.id) { - const res = checkAndUpdatePost(content) - if (res) { - getBlogPost(content.user, content.id, content) - } - } - } - } catch (error) { - } finally { - } - }, [hashMapPosts, favoritesLocal]) - return { - getBlogPosts, - getBlogPostsFavorites, - getBlogPostsSubscriptions, - checkAndUpdatePost, - getBlogPost, - hashMapPosts, - checkNewMessages, - getNewPosts, - getBlogFilteredPosts, - getMailMessages - } -} diff --git a/src/hooks/useFetchPosts.tsx b/src/hooks/useFetchPosts.tsx index f9ca156..73b5a2e 100644 --- a/src/hooks/useFetchPosts.tsx +++ b/src/hooks/useFetchPosts.tsx @@ -41,7 +41,7 @@ export const useFetchPosts = () => { const checkAndUpdatePost = React.useCallback( (post: BlogPost) => { // Check if the post exists in hashMapPosts - const existingPost = hashMapPosts[post.id] + const existingPost = hashMapPosts[post.id + "-" + post.user] if (!existingPost) { // If the post doesn't exist, add it to hashMapPosts return true diff --git a/src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx b/src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx index 7d64633..a551e4c 100644 --- a/src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx +++ b/src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx @@ -134,6 +134,8 @@ export const BlogIndividualProfile = () => { await getBlogPosts() }, [getBlogPosts]) + console.log({blogPosts}) + const subscribe = async () => { try { if (!user?.name) return @@ -233,7 +235,7 @@ export const BlogIndividualProfile = () => { style={{ backgroundColor: theme.palette.background.default }} > {blogPosts.map((post, index) => { - const existingPost = hashMapPosts[post.id] + let existingPost = hashMapPosts[post.id + "-" + post.user] let blogPost = post if (existingPost) { blogPost = existingPost diff --git a/src/pages/BlogList/BlogList.tsx b/src/pages/BlogList/BlogList.tsx index 118b1f0..02d1bef 100644 --- a/src/pages/BlogList/BlogList.tsx +++ b/src/pages/BlogList/BlogList.tsx @@ -155,7 +155,7 @@ export const BlogList = ({ mode }: BlogListProps) => { columnClassName="my-masonry-grid_column" > {posts.map((post, index) => { - const existingPost = hashMapPosts[post.id] + const existingPost = hashMapPosts[post.id + "-" + post.user] let blogPost = post if (existingPost) { blogPost = existingPost diff --git a/src/pages/CreatePost/CreatePost.tsx b/src/pages/CreatePost/CreatePost.tsx index e66783d..f11cd5e 100644 --- a/src/pages/CreatePost/CreatePost.tsx +++ b/src/pages/CreatePost/CreatePost.tsx @@ -1,5 +1,5 @@ import { Box, Button, Typography } from '@mui/material' -import React, { useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { ReusableModal } from '../../components/modals/ReusableModal' import { CreatePostBuilder } from './CreatePostBuilder' import { CreatePostMinimal } from './CreatePostMinimal' @@ -8,7 +8,7 @@ import HourglassFullRoundedIcon from '@mui/icons-material/HourglassFullRounded' import { display } from '@mui/system' import { useDispatch, useSelector } from 'react-redux' import { setIsLoadingGlobal } from '../../state/features/globalSlice' -import { useParams } from 'react-router-dom' +import { useNavigate, useParams } from 'react-router-dom' import { checkStructure } from '../../utils/checkStructure' import { RootState } from '../../state/store' import { @@ -28,7 +28,7 @@ export const CreatePost = ({ mode }: CreatePostProps) => { const formPostId = buildIdentifierFromCreateTitleIdAndId(formBlogId, postId) return formPostId }, [blog, postId, mode]) - const { user } = useSelector((state: RootState) => state.auth) + const user = useSelector((state: RootState) => state.auth?.user) const [toggleEditorType, setToggleEditorType] = useState<EditorType | null>( null @@ -38,6 +38,8 @@ export const CreatePost = ({ mode }: CreatePostProps) => { const [editType, setEditType] = useState<EditorType | null>(null) const [isOpen, setIsOpen] = useState<boolean>(false) const dispatch = useDispatch() + const navigate = useNavigate() + React.useEffect(() => { if (!toggleEditorType && mode !== 'edit') { setIsOpen(true) @@ -48,6 +50,14 @@ export const CreatePost = ({ mode }: CreatePostProps) => { setIsOpen(true) } + useEffect(()=> { + if(username && user?.name && mode === 'edit'){ + if(username !== user?.name){ + navigate('/') + } + } + }, [user, username, mode]) + const getBlogPost = React.useCallback(async () => { try { dispatch(setIsLoadingGlobal(true)) diff --git a/src/pages/Mail/AliasMail.tsx b/src/pages/Mail/AliasMail.tsx deleted file mode 100644 index 411863b..0000000 --- a/src/pages/Mail/AliasMail.tsx +++ /dev/null @@ -1,279 +0,0 @@ -import React, { - FC, - useCallback, - useEffect, - useMemo, - useRef, - useState -} from 'react' -import { useNavigate } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' -import { RootState } from '../../state/store' -import EditIcon from '@mui/icons-material/Edit' -import { Box, Button, Input, Typography, useTheme } from '@mui/material' -import { useFetchPosts } from '../../hooks/useFetchPosts' -import LazyLoad from '../../components/common/LazyLoad' -import { removePrefix } from '../../utils/blogIdformats' -import { NewMessage } from './NewMessage' -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, - setUserAvatarHash -} from '../../state/features/globalSlice' -import SimpleTable from './MailTable' -import { MAIL_SERVICE_TYPE } from '../../constants/mail' -import { BlogPost } from '../../state/features/blogSlice' - -interface AliasMailProps { - value: string -} -export const AliasMail = ({ value }: AliasMailProps) => { - const theme = useTheme() - const { user } = useSelector((state: RootState) => state.auth) - const [isOpen, setIsOpen] = useState<boolean>(false) - const [message, setMessage] = useState<any>(null) - const [replyTo, setReplyTo] = useState<any>(null) - const [valueTab, setValueTab] = React.useState(0) - const [aliasValue, setAliasValue] = useState('') - const [alias, setAlias] = useState<string[]>([]) - const hashMapPosts = useSelector( - (state: RootState) => state.blog.hashMapPosts - ) - const [mailMessages, setMailMessages] = useState<any[]>([]) - const hashMapMailMessages = useSelector( - (state: RootState) => state.mail.hashMapMailMessages - ) - - const fullMailMessages = useMemo(() => { - return mailMessages.map((msg) => { - let message = msg - const existingMessage = hashMapMailMessages[msg.id] - if (existingMessage) { - message = existingMessage - } - return message - }) - }, [mailMessages, hashMapMailMessages]) - const dispatch = useDispatch() - const navigate = useNavigate() - - const getAvatar = async (user: string) => { - try { - let url = await qortalRequest({ - action: 'GET_QDN_RESOURCE_URL', - name: user, - service: 'THUMBNAIL', - identifier: 'qortal_avatar' - }) - dispatch( - setUserAvatarHash({ - name: user, - url - }) - ) - } catch (error) {} - } - - const checkNewMessages = React.useCallback( - async (recipientName: string, recipientAddress: string) => { - try { - const query = `qortal_qmail_${value}_mail` - const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=20&includemetadata=true&reverse=true&excludeblocked=true` - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const latestPost = mailMessages[0] - if (!latestPost) return - const findPost = responseData?.findIndex( - (item: any) => item?.identifier === latestPost?.id - ) - if (findPost === -1) { - return - } - const newArray = responseData.slice(0, findPost) - const structureData = newArray.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: post?.created, - updated: post?.updated, - user: post.name, - id: post.identifier - } - }) - setMailMessages((prev) => { - const updatedMessages = [...prev] - - structureData.forEach((newMessage: any) => { - const existingIndex = updatedMessages.findIndex( - (prevMessage) => prevMessage.id === newMessage.id - ) - - if (existingIndex !== -1) { - // Replace existing message - updatedMessages[existingIndex] = newMessage - } else { - // Add new message - updatedMessages.unshift(newMessage) - } - }) - - return updatedMessages - }) - return - } catch (error) {} - }, - [mailMessages] - ) - - const getMailMessages = React.useCallback( - async (recipientName: string, recipientAddress: string) => { - try { - const offset = mailMessages.length - - dispatch(setIsLoadingGlobal(true)) - const query = `qortal_qmail_${value}_mail` - 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', - headers: { - 'Content-Type': 'application/json' - } - }) - const responseData = await response.json() - const structureData = responseData.map((post: any): BlogPost => { - return { - title: post?.metadata?.title, - category: post?.metadata?.category, - categoryName: post?.metadata?.categoryName, - tags: post?.metadata?.tags || [], - description: post?.metadata?.description, - createdAt: post?.created, - updated: post?.updated, - user: post.name, - id: post.identifier - } - }) - setMailMessages((prev) => { - const updatedMessages = [...prev] - - structureData.forEach((newMessage: any) => { - const existingIndex = updatedMessages.findIndex( - (prevMessage) => prevMessage.id === newMessage.id - ) - - if (existingIndex !== -1) { - // Replace existing message - updatedMessages[existingIndex] = newMessage - } else { - // Add new message - updatedMessages.push(newMessage) - } - }) - - return updatedMessages - }) - - for (const content of structureData) { - if (content.user && content.id) { - getAvatar(content.user) - } - } - } catch (error) { - } finally { - dispatch(setIsLoadingGlobal(false)) - } - }, - [mailMessages, hashMapMailMessages] - ) - const getMessages = React.useCallback(async () => { - if (!user?.name || !user?.address) return - await getMailMessages(user.name, user.address) - }, [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 openMessage = async ( - user: string, - messageIdentifier: string, - content: any - ) => { - try { - const existingMessage = hashMapMailMessages[messageIdentifier] - if (existingMessage) { - setMessage(existingMessage) - } - dispatch(setIsLoadingGlobal(true)) - const res = await fetchAndEvaluateMail({ - user, - messageIdentifier, - content, - otherUser: user - }) - setMessage(res) - dispatch(addToHashMapMail(res)) - setIsOpen(true) - } catch (error) { - } finally { - dispatch(setIsLoadingGlobal(false)) - } - } - - const firstMount = useRef(false) - useEffect(() => { - if (user?.name && !firstMount.current) { - getMessages() - firstMount.current = true - } - }, [user]) - - return ( - <> - <NewMessage replyTo={replyTo} setReplyTo={setReplyTo} alias={value} /> - <ShowMessage - isOpen={isOpen} - setIsOpen={setIsOpen} - message={message} - setReplyTo={setReplyTo} - alias={value} - /> - <SimpleTable - openMessage={openMessage} - data={fullMailMessages} - ></SimpleTable> - <LazyLoad onLoadMore={getMessages}></LazyLoad> - </> - ) -} diff --git a/src/pages/Mail/Mail.tsx b/src/pages/Mail/Mail.tsx deleted file mode 100644 index 15c793c..0000000 --- a/src/pages/Mail/Mail.tsx +++ /dev/null @@ -1,342 +0,0 @@ -import React, { - FC, - useCallback, - useEffect, - useMemo, - useRef, - useState -} from 'react' -import { useNavigate } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' -import { RootState } from '../../state/store' -import EditIcon from '@mui/icons-material/Edit' -import CloseIcon from '@mui/icons-material/Close' -import { - Box, - Button, - Input, - Typography, - useTheme, - IconButton -} from '@mui/material' -import { useFetchPosts } from '../../hooks/useFetchPosts' -import LazyLoad from '../../components/common/LazyLoad' -import { removePrefix } from '../../utils/blogIdformats' -import { NewMessage } from './NewMessage' -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' -import { AliasMail } from './AliasMail' - -export const Mail = () => { - const theme = useTheme() - const { user } = useSelector((state: RootState) => state.auth) - const [isOpen, setIsOpen] = useState<boolean>(false) - const [message, setMessage] = useState<any>(null) - const [replyTo, setReplyTo] = useState<any>(null) - const [valueTab, setValueTab] = React.useState(0) - const [aliasValue, setAliasValue] = useState('') - const [alias, setAlias] = useState<string[]>([]) - const hashMapPosts = useSelector( - (state: RootState) => state.blog.hashMapPosts - ) - const hashMapMailMessages = useSelector( - (state: RootState) => state.mail.hashMapMailMessages - ) - const mailMessages = useSelector( - (state: RootState) => state.mail.mailMessages - ) - - const fullMailMessages = useMemo(() => { - return mailMessages.map((msg) => { - let message = msg - const existingMessage = hashMapMailMessages[msg.id] - if (existingMessage) { - message = existingMessage - } - return message - }) - }, [mailMessages, hashMapMailMessages]) - const dispatch = useDispatch() - const navigate = useNavigate() - - const { getMailMessages, checkNewMessages } = useFetchMail() - const getMessages = React.useCallback(async () => { - if (!user?.name || !user?.address) return - await getMailMessages(user.name, user.address) - }, [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 openMessage = async ( - user: string, - messageIdentifier: string, - content: any - ) => { - try { - const existingMessage = hashMapMailMessages[messageIdentifier] - if (existingMessage) { - setMessage(existingMessage) - } - dispatch(setIsLoadingGlobal(true)) - const res = await fetchAndEvaluateMail({ - user, - messageIdentifier, - content, - otherUser: user - }) - setMessage(res) - dispatch(addToHashMapMail(res)) - setIsOpen(true) - } catch (error) { - } finally { - dispatch(setIsLoadingGlobal(false)) - } - } - - const firstMount = useRef(false) - useEffect(() => { - if (user?.name && !firstMount.current) { - getMessages() - firstMount.current = true - } - }, [user]) - - function a11yProps(index: number) { - return { - id: `mail-tabs-${index}`, - 'aria-controls': `mail-tabs-${index}` - } - } - - const handleChange = (event: React.SyntheticEvent, newValue: number) => { - setValueTab(newValue) - } - - function CustomTabLabel({ index, label }: any) { - return ( - <div style={{ display: 'flex', alignItems: 'center' }}> - <span>{label}</span> - <IconButton - edge="end" - color="inherit" - size="small" - onClick={(event) => { - setValueTab(0) - const newList = [...alias] - - newList.splice(index, 1) - - setAlias(newList) - }} - > - <CloseIcon fontSize="inherit" /> - </IconButton> - </div> - ) - } - - return ( - <Box - sx={{ - display: 'flex', - width: '100%', - flexDirection: 'column', - backgroundColor: 'background.paper' - }} - > - <Box - sx={{ - borderBottom: 1, - borderColor: 'divider', - display: 'flex', - width: '100%', - alignItems: 'center', - justifyContent: 'flex-start' - }} - > - <Tabs - value={valueTab} - onChange={handleChange} - aria-label="basic tabs example" - > - <Tab label={user?.name} {...a11yProps(0)} /> - {alias.map((alia, index) => { - return ( - <Tab - sx={{ - '&.Mui-selected': { - color: theme.palette.text.primary, - fontWeight: theme.typography.fontWeightMedium - } - }} - key={alia} - label={<CustomTabLabel index={index} label={alia} />} - {...a11yProps(1 + index)} - /> - ) - })} - </Tabs> - <Input - id="standard-adornment-alias" - onChange={(e) => { - setAliasValue(e.target.value) - }} - value={aliasValue} - placeholder="Type in alias" - sx={{ - marginLeft: '20px', - '&&:before': { - borderBottom: 'none' - }, - '&&:after': { - borderBottom: 'none' - }, - '&&:hover:before': { - borderBottom: 'none' - }, - '&&.Mui-focused:before': { - borderBottom: 'none' - }, - '&&.Mui-focused': { - outline: 'none' - }, - fontSize: '18px' - }} - /> - <Button - onClick={() => { - setAlias((prev) => [...prev, aliasValue]) - setAliasValue('') - }} - variant="contained" - > - + alias - </Button> - </Box> - <NewMessage replyTo={replyTo} setReplyTo={setReplyTo} /> - <ShowMessage - isOpen={isOpen} - setIsOpen={setIsOpen} - message={message} - setReplyTo={setReplyTo} - /> - {/* {countNewPosts > 0 && ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'center' - }} - > - <Typography> - {countNewPosts === 1 - ? `There is ${countNewPosts} new message` - : `There are ${countNewPosts} new messages`} - </Typography> - <Button - sx={{ - backgroundColor: theme.palette.primary.light, - color: theme.palette.text.primary, - fontFamily: 'Arial' - }} - onClick={getNewPosts} - > - Load new Posts - </Button> - </Box> - )} */} - <TabPanel value={valueTab} index={0}> - <SimpleTable - openMessage={openMessage} - data={fullMailMessages} - ></SimpleTable> - <LazyLoad onLoadMore={getMessages}></LazyLoad> - </TabPanel> - {alias.map((alia, index) => { - return ( - <TabPanel key={alia} value={valueTab} index={1 + index}> - <AliasMail value={alia} /> - </TabPanel> - ) - })} - - {/* <Box> - {mailMessages.map((message, index) => { - const existingMessage = hashMapMailMessages[message.id] - let mailMessage = message - if (existingMessage) { - mailMessage = existingMessage - } - return ( - <Box - sx={{ - display: 'flex', - gap: 1, - alignItems: 'center', - width: 'auto', - position: 'relative', - ' @media (max-width: 450px)': { - width: '100%' - } - }} - key={mailMessage.id} - > - hello - </Box> - ) - })} - </Box> */} - </Box> - ) -} - -interface TabPanelProps { - children?: React.ReactNode - index: number - value: number -} - -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> - ) -} \ No newline at end of file diff --git a/src/pages/Mail/MailTable.tsx b/src/pages/Mail/MailTable.tsx deleted file mode 100644 index a678e6d..0000000 --- a/src/pages/Mail/MailTable.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import * as React from 'react' -import Table from '@mui/material/Table' -import TableBody from '@mui/material/TableBody' -import TableCell from '@mui/material/TableCell' -import TableContainer from '@mui/material/TableContainer' -import TableHead from '@mui/material/TableHead' -import TableRow from '@mui/material/TableRow' -import Paper from '@mui/material/Paper' -import { Avatar, Box } from '@mui/material' -import { useSelector } from 'react-redux' -import { RootState } from '../../state/store' -import { formatTimestamp } from '../../utils/time' - -const tableCellFontSize = '16px' - -interface Data { - name: string - description: string - createdAt: number - user: string - id: string - tags: string[] - subject?: string -} - -interface ColumnData { - dataKey: keyof Data - label: string - numeric?: boolean - width?: number -} - -const columns: ColumnData[] = [ - { - label: 'Sender', - dataKey: 'user', - width: 200 - }, - { - label: 'Subject', - dataKey: 'description' - }, - { - label: 'Date', - dataKey: 'createdAt', - numeric: true, - width: 200 - } -] - -// Replace this with your own data -const rows: Data[] = [ - { - name: 'Sample 1', - description: 'Sample description 1', - createdAt: 1682857406070, - user: 'tester1', - id: 'qortal_qmail_Phil_ViVrF2_mail_NnHcWj', - tags: ['attach: 0'] - }, - { - name: 'Sample 2', - description: 'Sample description 2', - createdAt: 1682857406071, - user: 'tester2', - id: 'qortal_qmail_Phil_ViVrF2_mail_NnHcWk', - tags: ['attach: 1'] - } - // Add more rows as needed -] - -function fixedHeaderContent() { - return ( - <TableRow> - {columns.map((column) => { - return ( - <TableCell - key={column.dataKey} - variant="head" - align={column.numeric || false ? 'right' : 'left'} - style={{ width: column.width }} - sx={{ - backgroundColor: 'background.paper', - fontSize: tableCellFontSize, - padding: '7px' - }} - > - {column.label} - </TableCell> - ) - })} - </TableRow> - ) -} - -function rowContent(_index: number, row: Data, openMessage: any) { - return ( - <React.Fragment> - {columns.map((column) => { - let subject = '-' - if (column.dataKey === 'description' && row['subject']) { - subject = row['subject'] - } - return ( - <TableCell - onClick={() => openMessage(row?.user, row?.id, row)} - key={column.dataKey} - align={column.numeric || false ? 'right' : 'left'} - style={{ width: column.width, cursor: 'pointer' }} - sx={{ - fontSize: tableCellFontSize, - padding: '7px' - }} - > - {column.dataKey === 'user' && ( - <Box - sx={{ - display: 'flex', - gap: '5px', - width: '100%', - alignItems: 'center', - flexWrap: 'wrap', - textOverflow: 'ellipsis', - overflow: 'hidden', - whiteSpace: 'nowrap' - }} - > - <AvatarWrapper user={row?.user}></AvatarWrapper> - {row[column.dataKey]} - </Box> - )} - {column.dataKey !== 'user' && ( - <> - {column.dataKey === 'createdAt' - ? formatTimestamp(row[column.dataKey]) - : column.dataKey === 'description' - ? subject - : row[column.dataKey]} - </> - )} - </TableCell> - ) - })} - </React.Fragment> - ) -} - -interface SimpleTableProps { - openMessage: (user: string, messageIdentifier: string, content: any) => void - data: Data[] - children?: React.ReactNode -} - -export default function SimpleTable({ - openMessage, - data, - children -}: SimpleTableProps) { - return ( - <Paper style={{ width: '100%' }}> - <TableContainer component={Paper}> - <Table> - <TableHead>{fixedHeaderContent()}</TableHead> - <TableBody> - {data.map((row, index) => ( - <TableRow key={index}> - {rowContent(index, row, openMessage)} - </TableRow> - ))} - </TableBody> - </Table> - </TableContainer> - {children} - </Paper> - ) -} - -export const AvatarWrapper = ({ user }: any) => { - const userAvatarHash = useSelector( - (state: RootState) => state.global.userAvatarHash - ) - const avatarLink = React.useMemo(() => { - if (!user || !userAvatarHash) return '' - const findUserAvatar = userAvatarHash[user] - if (!findUserAvatar) return '' - return findUserAvatar - }, [userAvatarHash, user]) - - return <Avatar src={avatarLink} alt={`${user}'s avatar`} /> -} diff --git a/src/pages/Mail/MailThread.tsx b/src/pages/Mail/MailThread.tsx deleted file mode 100644 index 7c726cd..0000000 --- a/src/pages/Mail/MailThread.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import * as React from 'react' -import { styled } from '@mui/material/styles' -import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp' -import MuiAccordion, { AccordionProps } from '@mui/material/Accordion' -import MuiAccordionSummary, { - AccordionSummaryProps -} from '@mui/material/AccordionSummary' -import MuiAccordionDetails from '@mui/material/AccordionDetails' -import Typography from '@mui/material/Typography' -import { Box, CircularProgress } from '@mui/material' -import { useDispatch, useSelector } from 'react-redux' -import { RootState } from '../../state/store' -import { formatTimestamp } from '../../utils/time' -import ReadOnlySlate from '../../components/editor/ReadOnlySlate' -import { fetchAndEvaluateMail } from '../../utils/fetchMail' -import { addToHashMapMail } from '../../state/features/mailSlice' -import { AvatarWrapper } from './MailTable' -import FileElement from '../../components/FileElement' -import AttachFileIcon from '@mui/icons-material/AttachFile' -import { MAIL_SERVICE_TYPE } from '../../constants/mail' - -const Accordion = styled((props: AccordionProps) => ( - <MuiAccordion disableGutters elevation={0} square {...props} /> -))(({ theme }) => ({ - border: `1px solid ${theme.palette.divider}`, - '&:not(:last-child)': { - borderBottom: 0 - }, - '&:before': { - display: 'none' - } -})) - -const AccordionSummary = styled((props: AccordionSummaryProps) => ( - <MuiAccordionSummary - expandIcon={<ArrowForwardIosSharpIcon sx={{ fontSize: '16px' }} />} - {...props} - /> -))(({ theme }) => ({ - backgroundColor: - theme.palette.mode === 'dark' - ? 'rgba(255, 255, 255, .05)' - : 'rgba(0, 0, 0, .03)', - flexDirection: 'row-reverse', - '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { - transform: 'rotate(90deg)' - }, - '& .MuiAccordionSummary-content': { - marginLeft: theme.spacing(1) - } -})) - -const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({ - padding: theme.spacing(2), - borderTop: '1px solid rgba(0, 0, 0, .125)' -})) - -interface IThread { - identifier: string - service: string - name: string -} - -export default function MailThread({ - thread, - users, - otherUser -}: { - thread: IThread[] - users: string[] - otherUser: string -}) { - const [expanded, setExpanded] = React.useState<string | false>('panel1') - const [isLoading, setIsLoading] = React.useState<boolean>(false) - const dispatch = useDispatch() - const hashMapMailMessages = useSelector( - (state: RootState) => state.mail.hashMapMailMessages - ) - const handleChange = - (panel: string) => (event: React.SyntheticEvent, newExpanded: boolean) => { - setExpanded(newExpanded ? panel : false) - } - const getThreadMessages = async () => { - setIsLoading(true) - try { - for (const msgId of thread) { - const existingMessage = hashMapMailMessages[msgId?.identifier] - if (existingMessage) { - } else { - try { - const query = msgId?.identifier - const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=20&includemetadata=true&offset=0&reverse=true&excludeblocked=true&name=${msgId?.name}&exactmatchnames=true&` - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - - const responseData = await response.json() - if (responseData.length !== 0) { - const data = responseData[0] - const content = { - title: data?.metadata?.title, - category: data?.metadata?.category, - categoryName: data?.metadata?.categoryName, - tags: data?.metadata?.tags || [], - description: data?.metadata?.description, - createdAt: data?.created, - updated: data?.updated, - user: data.name, - id: data.identifier - } - const res = await fetchAndEvaluateMail({ - user: data.name, - messageIdentifier: data.identifier, - content, - otherUser - }) - - dispatch(addToHashMapMail(res)) - } - } catch (error) {} - } - } - } catch (error) {} - setIsLoading(false) - } - - React.useEffect(() => { - getThreadMessages() - }, []) - - if (isLoading) return <CircularProgress color="secondary" /> - return ( - <Box - sx={{ - width: '100%' - }} - > - {thread?.map((message: any) => { - const findMessage: any = hashMapMailMessages[message?.identifier] - if (!findMessage) return null - - return ( - <Accordion - expanded={expanded === message?.identifier} - onChange={handleChange(message?.identifier)} - > - <AccordionSummary - aria-controls="panel1d-content" - id="panel1d-header" - sx={{ - fontSize: '16px', - height: '36px' - }} - > - <Box - sx={{ - width: '100%', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between' - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: '10px' - }} - > - <AvatarWrapper user={findMessage?.user} /> - <Typography - sx={{ - fontSize: '16px' - }} - > - {findMessage?.user} - </Typography> - <Typography>{findMessage?.description}</Typography> - </Box> - <Box - sx={{ - display: 'flex', - alignItems: 'center' - }} - > - <Typography - sx={{ - fontSize: '16px' - }} - > - {formatTimestamp(findMessage?.createdAt)} - </Typography> - </Box> - </Box> - </AccordionSummary> - <AccordionDetails> - <> - {findMessage?.attachments?.length > 0 && ( - <Box - sx={{ - width: '100%', - marginTop: '10px', - marginBottom: '20px' - }} - > - {findMessage?.attachments.map((file: any) => { - return ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-start', - width: '100%' - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: '5px', - cursor: 'pointer', - width: 'auto' - }} - > - <FileElement - fileInfo={file} - title={file?.filename} - mode="mail" - otherUser={otherUser} - > - <AttachFileIcon - sx={{ - height: '16px', - width: 'auto' - }} - ></AttachFileIcon> - <Typography - sx={{ - fontSize: '16px' - }} - > - {file?.originalFilename || file?.filename} - </Typography> - </FileElement> - </Box> - </Box> - ) - })} - </Box> - )} - {findMessage?.textContent && ( - <ReadOnlySlate - content={findMessage.textContent} - mode="mail" - /> - )} - </> - </AccordionDetails> - </Accordion> - ) - })} - {/* <Accordion - expanded={expanded === 'panel1'} - onChange={handleChange('panel1')} - > - <AccordionSummary aria-controls="panel1d-content" id="panel1d-header"> - <Typography>Collapsible Group Item #1</Typography> - </AccordionSummary> - <AccordionDetails> - <Typography> - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse - malesuada lacus ex, sit amet blandit leo lobortis eget. Lorem ipsum - dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada - lacus ex, sit amet blandit leo lobortis eget. - </Typography> - </AccordionDetails> - </Accordion> - <Accordion - expanded={expanded === 'panel2'} - onChange={handleChange('panel2')} - > - <AccordionSummary aria-controls="panel2d-content" id="panel2d-header"> - <Typography>Collapsible Group Item #2</Typography> - </AccordionSummary> - <AccordionDetails> - <Typography> - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse - malesuada lacus ex, sit amet blandit leo lobortis eget. Lorem ipsum - dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada - lacus ex, sit amet blandit leo lobortis eget. - </Typography> - </AccordionDetails> - </Accordion> - <Accordion - expanded={expanded === 'panel3'} - onChange={handleChange('panel3')} - > - <AccordionSummary aria-controls="panel3d-content" id="panel3d-header"> - <Typography>Collapsible Group Item #3</Typography> - </AccordionSummary> - <AccordionDetails> - <Typography> - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse - malesuada lacus ex, sit amet blandit leo lobortis eget. Lorem ipsum - dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada - lacus ex, sit amet blandit leo lobortis eget. - </Typography> - </AccordionDetails> - </Accordion> */} - </Box> - ) -} diff --git a/src/pages/Mail/NewMessage.tsx b/src/pages/Mail/NewMessage.tsx deleted file mode 100644 index e1fab35..0000000 --- a/src/pages/Mail/NewMessage.tsx +++ /dev/null @@ -1,425 +0,0 @@ -import React, { Dispatch, useEffect, useState } from 'react' -import { ReusableModal } from '../../components/modals/ReusableModal' -import { Box, Input, Typography } from '@mui/material' -import { BuilderButton } from '../CreatePost/CreatePost-styles' -import BlogEditor from '../../components/editor/BlogEditor' -import EmailIcon from '@mui/icons-material/Email' -import { Descendant } from 'slate' -import ShortUniqueId from 'short-unique-id' -import { useDispatch, useSelector } from 'react-redux' -import { RootState } from '../../state/store' -import { useDropzone } from 'react-dropzone' -import AttachFileIcon from '@mui/icons-material/AttachFile' -import CloseIcon from '@mui/icons-material/Close' - -import { setNotification } from '../../state/features/notificationsSlice' -import { - objectToBase64, - objectToUint8Array, - objectToUint8ArrayFromResponse, - processFileInChunks, - toBase64, - uint8ArrayToBase64 -} from '../../utils/toBase64' -import { - MAIL_ATTACHMENT_SERVICE_TYPE, - MAIL_SERVICE_TYPE -} from '../../constants/mail' -const initialValue: Descendant[] = [ - { - type: 'paragraph', - children: [{ text: '' }] - } -] -const uid = new ShortUniqueId() - -interface NewMessageProps { - replyTo?: any - setReplyTo: React.Dispatch<any> - alias?: string -} -const maxSize = 25 * 1024 * 1024 // 25 MB in bytes -export const NewMessage = ({ setReplyTo, replyTo, alias }: NewMessageProps) => { - const [isOpen, setIsOpen] = useState<boolean>(false) - const [value, setValue] = useState(initialValue) - const [title, setTitle] = useState<string>('') - const [attachments, setAttachments] = useState<any[]>([]) - const [description, setDescription] = useState<string>('') - const [subject, setSubject] = useState<string>('') - const [destinationName, setDestinationName] = useState('') - const [aliasValue, setAliasValue] = useState<string>('') - const { user } = useSelector((state: RootState) => state.auth) - const dispatch = useDispatch() - const { getRootProps, getInputProps } = useDropzone({ - maxSize, - onDrop: (acceptedFiles) => { - setAttachments((prev) => [...prev, ...acceptedFiles]) - }, - onDropRejected: (rejectedFiles) => { - dispatch( - setNotification({ - msg: 'One of your files is over the 25mb limit', - alertType: 'error' - }) - ) - } - }) - - useEffect(() => { - if (alias) { - setAliasValue(alias) - } - }, [alias]) - - const openModal = () => { - setIsOpen(true) - - setReplyTo(null) - } - const closeModal = () => { - setAttachments([]) - setSubject('') - setDestinationName('') - - setValue(initialValue) - setReplyTo(null) - setIsOpen(false) - if (!alias) { - setAliasValue('') - } - } - useEffect(() => { - if (replyTo) { - setIsOpen(true) - setDestinationName(replyTo?.user || '') - } - }, [replyTo]) - async function publishQDNResource() { - let address: string = '' - let name: string = '' - let errorMsg = '' - - address = user?.address || '' - name = user?.name || '' - - const missingFields: string[] = [] - if (!address) { - errorMsg = "Cannot send: your address isn't available" - } - if (!name) { - errorMsg = 'Cannot send a message without a access to your name' - } - if (!destinationName) { - errorMsg = 'Cannot send a message without a recipient name' - } - // if (!description) missingFields.push('subject') - if (missingFields.length > 0) { - const missingFieldsString = missingFields.join(', ') - const errMsg = `Missing: ${missingFieldsString}` - errorMsg = errMsg - } - - if (errorMsg) { - dispatch( - setNotification({ - msg: errorMsg, - alertType: 'error' - }) - ) - throw new Error(errorMsg) - } - - const mailObject: any = { - title, - // description, - subject, - createdAt: Date.now(), - version: 1, - attachments, - textContent: value, - generalData: { - thread: [] - }, - recipient: destinationName - } - if (replyTo?.id) { - const previousTread = Array.isArray(replyTo?.generalData?.thread) - ? replyTo?.generalData?.thread - : [] - mailObject.generalData.thread = [ - ...previousTread, - { - identifier: replyTo.id, - name: replyTo.user, - service: MAIL_SERVICE_TYPE - } - ] - } - - try { - if (!destinationName) return - const id = uid() - const recipientName = destinationName - const resName = await qortalRequest({ - action: 'GET_NAME_DATA', - name: recipientName - }) - if (!resName?.owner) return - - const recipientAddress = resName.owner - const resAddress = await qortalRequest({ - action: 'GET_ACCOUNT_DATA', - address: recipientAddress - }) - if (!resAddress?.publicKey) return - const recipientPublicKey = resAddress.publicKey - - // START OF ATTACHMENT LOGIC - - const attachmentArray = [] - for (const attachment of attachments) { - const fileBase64 = await toBase64(attachment) - if (typeof fileBase64 !== 'string' || !fileBase64) - throw new Error('Could not convert file to base64') - const base64String = fileBase64.split(',')[1] - - const id = uid() - const id2 = uid() - const identifier = `attachments_qmail_${id}_${id2}` - const fileExtension = attachment?.name?.split('.')?.pop() - if (!fileExtension) { - throw new Error('One of your attachments does not have an extension') - } - const obj = { - name: name, - service: MAIL_ATTACHMENT_SERVICE_TYPE, - filename: `${id}.${fileExtension}`, - identifier, - data64: base64String - } - - attachmentArray.push(obj) - } - - if (attachmentArray?.length > 0) { - mailObject.attachments = attachmentArray.map((item) => { - return { - identifier: item.identifier, - name, - service: MAIL_ATTACHMENT_SERVICE_TYPE, - filename: item.filename - } - }) - - const multiplePublish = { - action: 'PUBLISH_MULTIPLE_QDN_RESOURCES', - resources: [...attachmentArray], - encrypt: true, - recipientPublicKey - } - await qortalRequest(multiplePublish) - } - - //END OF ATTACHMENT LOGIC - - const blogPostToBase64 = await objectToBase64(mailObject) - let identifier = `qortal_qmail_${recipientName.slice( - 0, - 20 - )}_${recipientAddress.slice(-6)}_mail_${id}` - - if (aliasValue) { - identifier = `qortal_qmail_${aliasValue}_mail_${id}` - } - - let requestBody: any = { - action: 'PUBLISH_QDN_RESOURCE', - name: name, - service: MAIL_SERVICE_TYPE, - data64: blogPostToBase64, - title: title, - // description: description, - identifier, - encrypt: true, - recipientPublicKey - } - - await qortalRequest(requestBody) - dispatch( - setNotification({ - msg: 'Message sent', - alertType: 'success' - }) - ) - - closeModal() - } catch (error: any) { - let notificationObj = null - if (typeof error === 'string') { - notificationObj = { - msg: error || 'Failed to send message', - alertType: 'error' - } - } else if (typeof error?.error === 'string') { - notificationObj = { - msg: error?.error || 'Failed to send message', - alertType: 'error' - } - } else { - notificationObj = { - msg: error?.message || 'Failed to send message', - alertType: 'error' - } - } - if (!notificationObj) return - dispatch(setNotification(notificationObj)) - - throw new Error('Failed to send message') - } - } - - const sendMail = () => { - publishQDNResource() - } - return ( - <Box - sx={{ - display: 'flex', - justifyContent: 'flex-end', - width: '100%' - }} - > - {!alias && ( - <EmailIcon - sx={{ - cursor: 'pointer', - margin: '15px' - }} - onClick={openModal} - /> - )} - - <ReusableModal open={isOpen}> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - flexDirection: 'column', - gap: 1 - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'flex-start', - flexDirection: 'column', - gap: 2, - width: '100%' - }} - > - <Input - id="standard-adornment-name" - value={destinationName} - disabled={!!replyTo} - onChange={(e) => { - setDestinationName(e.target.value) - }} - placeholder="To (name) -public" - sx={{ - width: '100%', - fontSize: '16px' - }} - /> - <Input - id="standard-adornment-alias" - value={aliasValue} - disabled={!!alias} - onChange={(e) => { - setAliasValue(e.target.value) - }} - placeholder="Alias -optional" - sx={{ - width: '100%', - fontSize: '16px' - }} - /> - - <Input - id="standard-adornment-name" - value={subject} - onChange={(e) => { - setSubject(e.target.value) - }} - placeholder="Subject" - sx={{ - width: '100%', - fontSize: '16px' - }} - /> - <Box - {...getRootProps()} - sx={{ - border: '1px dashed gray', - padding: 2, - textAlign: 'center', - marginBottom: 2 - }} - > - <input {...getInputProps()} /> - <AttachFileIcon - sx={{ - height: '20px', - width: 'auto', - cursor: 'pointer' - }} - ></AttachFileIcon> - </Box> - <Box> - {attachments.map((file, index) => { - return ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: '15px' - }} - > - <Typography - sx={{ - fontSize: '16px' - }} - > - {file?.name} - </Typography> - <CloseIcon - onClick={() => - setAttachments((prev) => - prev.filter((item, itemIndex) => itemIndex !== index) - ) - } - sx={{ - height: '16px', - width: 'auto', - cursor: 'pointer' - }} - /> - </Box> - ) - })} - </Box> - </Box> - <BlogEditor - mode="mail" - value={value} - setValue={setValue} - editorKey={1} - /> - </Box> - <BuilderButton onClick={sendMail}> - {replyTo ? 'Send reply mail' : 'Send mail'} - </BuilderButton> - <BuilderButton onClick={closeModal}>Close</BuilderButton> - </ReusableModal> - </Box> - ) -} diff --git a/src/pages/Mail/ShowMessage.tsx b/src/pages/Mail/ShowMessage.tsx deleted file mode 100644 index ad9e29f..0000000 --- a/src/pages/Mail/ShowMessage.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import React, { useState } from 'react' -import { ReusableModal } from '../../components/modals/ReusableModal' -import { Box, Button, Input, Typography } from '@mui/material' -import { BuilderButton } from '../CreatePost/CreatePost-styles' -import BlogEditor from '../../components/editor/BlogEditor' -import EmailIcon from '@mui/icons-material/Email' -import { Descendant } from 'slate' -import ShortUniqueId from 'short-unique-id' -import { useDispatch, useSelector } from 'react-redux' -import { RootState } from '../../state/store' -import AttachFileIcon from '@mui/icons-material/AttachFile' - -import { setNotification } from '../../state/features/notificationsSlice' -import { - objectToBase64, - objectToUint8Array, - objectToUint8ArrayFromResponse, - uint8ArrayToBase64 -} from '../../utils/toBase64' -import ReadOnlySlate from '../../components/editor/ReadOnlySlate' -import MailThread from './MailThread' -import { AvatarWrapper } from './MailTable' -import { formatTimestamp } from '../../utils/time' -import FileElement from '../../components/FileElement' -const initialValue: Descendant[] = [ - { - type: 'paragraph', - children: [{ text: '' }] - } -] -const uid = new ShortUniqueId() - -export const ShowMessage = ({ - isOpen, - setIsOpen, - message, - setReplyTo, - alias -}: any) => { - const [value, setValue] = useState(initialValue) - const [title, setTitle] = useState<string>('') - const [attachments, setAttachments] = useState<any[]>([]) - const [description, setDescription] = useState<string>('') - const [isOpenMailThread, setIsOpenMailThread] = useState<boolean>(false) - - const [destinationName, setDestinationName] = useState('') - const user = useSelector((state: RootState) => state.auth?.user) - const dispatch = useDispatch() - const openModal = () => { - setIsOpen(true) - } - const closeModal = () => { - setIsOpen(false) - setIsOpenMailThread(false) - } - - const handleReply = () => { - setReplyTo(message) - } - - return ( - <Box - sx={{ - display: 'flex', - justifyContent: 'flex-end', - width: '100%' - }} - > - <ReusableModal - open={isOpen} - customStyles={{ - width: '96%', - maxWidth: 1500, - height: '96%' - }} - > - <Box - sx={{ - display: 'flex', - justifyContent: 'flex-end', - width: '100%', - alignItems: 'center' - }} - > - {isOpenMailThread && - !alias && - message?.generalData?.thread && - message?.user && - user?.name && ( - <Button - variant="contained" - onClick={() => { - setIsOpenMailThread(false) - }} - > - Hide message threads - </Button> - )} - - {!isOpenMailThread && - !alias && - message?.generalData?.thread?.length > 0 && - message?.user && - user?.name && ( - <Button - variant="contained" - onClick={() => { - setIsOpenMailThread(true) - }} - > - Show message threads - </Button> - )} - </Box> - - <Box - sx={{ - display: 'flex', - alignItems: 'center', - flexDirection: 'column', - gap: 1, - flexGrow: 1, - overflow: 'auto', - width: '100%' - }} - > - {isOpenMailThread && - !alias && - message?.generalData?.thread?.length > 0 && - message?.user && - user?.name && ( - <MailThread - thread={message?.generalData?.thread} - users={[message.user, user.name]} - otherUser={message?.user} - /> - )} - <Box - sx={{ - display: 'flex', - gap: 1, - justifyContent: 'space-between', - alignItems: 'center', - width: '100%' - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: '10px' - }} - > - <AvatarWrapper user={message?.user} /> - <Typography - sx={{ - fontSize: '16px' - }} - > - {message?.user} - </Typography> - </Box> - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: '10px' - }} - > - <Typography - sx={{ - fontSize: '16px' - }} - > - {message?.subject} - </Typography> - <Typography - sx={{ - fontSize: '16px' - }} - > - {formatTimestamp(message?.createdAt)} - </Typography> - </Box> - </Box> - {message?.attachments?.length > 0 && ( - <Box - sx={{ - width: '100%', - marginTop: '10px' - }} - > - {message?.attachments.map((file: any) => { - return ( - <Box - sx={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'flex-start', - width: '100%' - }} - > - <Box - sx={{ - display: 'flex', - alignItems: 'center', - gap: '5px', - cursor: 'pointer', - width: 'auto' - }} - > - <FileElement - fileInfo={file} - title={file?.filename} - mode="mail" - otherUser={message?.user} - > - <AttachFileIcon - sx={{ - height: '16px', - width: 'auto' - }} - ></AttachFileIcon> - <Typography - sx={{ - fontSize: '16px' - }} - > - {file?.originalFilename || file?.filename} - </Typography> - </FileElement> - </Box> - </Box> - ) - })} - </Box> - )} - - {message?.textContent && ( - <ReadOnlySlate content={message.textContent} mode="mail" /> - )} - </Box> - <Box - sx={{ - display: 'flex', - gap: 1, - justifyContent: 'flex-end' - }} - > - <BuilderButton onClick={handleReply}>Reply</BuilderButton> - <BuilderButton onClick={closeModal}>Close</BuilderButton> - </Box> - </ReusableModal> - </Box> - ) -} diff --git a/src/state/features/blogSlice.ts b/src/state/features/blogSlice.ts index 3395400..d6a697f 100644 --- a/src/state/features/blogSlice.ts +++ b/src/state/features/blogSlice.ts @@ -178,12 +178,13 @@ export const blogSlice = createSlice({ }, addToHashMap: (state, action) => { const post = action.payload - state.hashMapPosts[post.id] = post + const fullId = + state.hashMapPosts[post.id + "-" + post.user] = post }, updateInHashMap: (state, action) => { - const { id } = action.payload + const { id, user } = action.payload const post = action.payload - state.hashMapPosts[id] = { ...post } + state.hashMapPosts[id + '-' + user] = { ...post } }, removeFromHashMap: (state, action) => { const idToDelete = action.payload @@ -192,7 +193,7 @@ export const blogSlice = createSlice({ addArrayToHashMap: (state, action) => { const posts = action.payload posts.forEach((post: BlogPost) => { - state.hashMapPosts[post.id] = post + state.hashMapPosts[post.id + "-" + post.user] = post }) }, upsertPosts: (state, action) => { diff --git a/src/utils/checkAndUpdatePost.tsx b/src/utils/checkAndUpdatePost.tsx index f35473a..bc64238 100644 --- a/src/utils/checkAndUpdatePost.tsx +++ b/src/utils/checkAndUpdatePost.tsx @@ -7,7 +7,7 @@ export const checkAndUpdatePost = (post: BlogPost) => { const hashMapPosts = useSelector((state: RootState) => state.blog.hashMapPosts); // Check if the post exists in hashMapPosts - const existingPost = hashMapPosts[post.id]; + const existingPost = hashMapPosts[post.id + "-" + post.user]; if (!existingPost) { // If the post doesn't exist, add it to hashMapPosts