mirror of
https://github.com/Qortal/q-blog.git
synced 2025-01-30 14:52:19 +00:00
fix id issue
This commit is contained in:
parent
7391940b13
commit
a1d8f706b8
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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`} />
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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) => {
|
||||
|
@ -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…
Reference in New Issue
Block a user