Browse Source

fix id issue

pull/1/head
PhilReact 8 months ago
parent
commit
a1d8f706b8
  1. 2
      src/App.tsx
  2. 469
      src/hooks/useFetchMail.tsx
  3. 2
      src/hooks/useFetchPosts.tsx
  4. 4
      src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx
  5. 2
      src/pages/BlogList/BlogList.tsx
  6. 16
      src/pages/CreatePost/CreatePost.tsx
  7. 279
      src/pages/Mail/AliasMail.tsx
  8. 342
      src/pages/Mail/Mail.tsx
  9. 190
      src/pages/Mail/MailTable.tsx
  10. 315
      src/pages/Mail/MailThread.tsx
  11. 425
      src/pages/Mail/NewMessage.tsx
  12. 256
      src/pages/Mail/ShowMessage.tsx
  13. 9
      src/state/features/blogSlice.ts
  14. 2
      src/utils/checkAndUpdatePost.tsx

2
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>

469
src/hooks/useFetchMail.tsx

@ -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
}
}

2
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

4
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

2
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

16
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))

279
src/pages/Mail/AliasMail.tsx

@ -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>
</>
)
}

342
src/pages/Mail/Mail.tsx

@ -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>
)
}

190
src/pages/Mail/MailTable.tsx

@ -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`} />
}

315
src/pages/Mail/MailThread.tsx

@ -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>
)
}

425
src/pages/Mail/NewMessage.tsx

@ -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>
)
}

256
src/pages/Mail/ShowMessage.tsx

@ -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>
)
}

9
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) => {

2
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

Loading…
Cancel
Save