mirror of
https://github.com/beilunyang/moemail.git
synced 2026-05-11 18:11:27 +08:00
feat: implement email sending functionality via Resend service
This commit is contained in:
76
app/components/emails/message-list-container.tsx
Normal file
76
app/components/emails/message-list-container.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Send, Inbox } from "lucide-react"
|
||||
import { Tabs, SlidingTabsList, SlidingTabsTrigger, TabsContent } from "@/components/ui/tabs"
|
||||
import { MessageList } from "./message-list"
|
||||
import { useSendPermission } from "@/hooks/use-send-permission"
|
||||
|
||||
interface MessageListContainerProps {
|
||||
email: {
|
||||
id: string
|
||||
address: string
|
||||
}
|
||||
onMessageSelect: (messageId: string | null, messageType?: 'received' | 'sent') => void
|
||||
selectedMessageId?: string | null
|
||||
refreshTrigger?: number
|
||||
}
|
||||
|
||||
export function MessageListContainer({ email, onMessageSelect, selectedMessageId, refreshTrigger }: MessageListContainerProps) {
|
||||
const [activeTab, setActiveTab] = useState<'received' | 'sent'>('received')
|
||||
const { canSend: canSendEmails } = useSendPermission()
|
||||
|
||||
const handleTabChange = (tabId: string) => {
|
||||
setActiveTab(tabId as 'received' | 'sent')
|
||||
onMessageSelect(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{canSendEmails ? (
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange} className="h-full flex flex-col">
|
||||
<div className="p-2 border-b border-primary/20">
|
||||
<SlidingTabsList>
|
||||
<SlidingTabsTrigger value="received">
|
||||
<Inbox className="h-4 w-4" />
|
||||
收件箱
|
||||
</SlidingTabsTrigger>
|
||||
<SlidingTabsTrigger value="sent">
|
||||
<Send className="h-4 w-4" />
|
||||
已发送
|
||||
</SlidingTabsTrigger>
|
||||
</SlidingTabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value="received" className="flex-1 overflow-hidden m-0">
|
||||
<MessageList
|
||||
email={email}
|
||||
messageType="received"
|
||||
onMessageSelect={onMessageSelect}
|
||||
selectedMessageId={selectedMessageId}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="sent" className="flex-1 overflow-hidden m-0">
|
||||
<MessageList
|
||||
email={email}
|
||||
messageType="sent"
|
||||
onMessageSelect={onMessageSelect}
|
||||
selectedMessageId={selectedMessageId}
|
||||
refreshTrigger={refreshTrigger}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<MessageList
|
||||
email={email}
|
||||
messageType="received"
|
||||
onMessageSelect={onMessageSelect}
|
||||
selectedMessageId={selectedMessageId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -20,9 +20,13 @@ import {
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
from_address: string
|
||||
from_address?: string
|
||||
to_address?: string
|
||||
subject: string
|
||||
received_at: number
|
||||
received_at?: number
|
||||
sent_at?: number
|
||||
content?: string
|
||||
html?: string
|
||||
}
|
||||
|
||||
interface MessageListProps {
|
||||
@@ -30,8 +34,10 @@ interface MessageListProps {
|
||||
id: string
|
||||
address: string
|
||||
}
|
||||
onMessageSelect: (messageId: string | null) => void
|
||||
messageType: 'received' | 'sent'
|
||||
onMessageSelect: (messageId: string | null, messageType?: 'received' | 'sent') => void
|
||||
selectedMessageId?: string | null
|
||||
refreshTrigger?: number
|
||||
}
|
||||
|
||||
interface MessageResponse {
|
||||
@@ -40,7 +46,7 @@ interface MessageResponse {
|
||||
total: number
|
||||
}
|
||||
|
||||
export function MessageList({ email, onMessageSelect, selectedMessageId }: MessageListProps) {
|
||||
export function MessageList({ email, messageType, onMessageSelect, selectedMessageId, refreshTrigger }: MessageListProps) {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
@@ -60,6 +66,9 @@ export function MessageList({ email, onMessageSelect, selectedMessageId }: Messa
|
||||
const fetchMessages = async (cursor?: string) => {
|
||||
try {
|
||||
const url = new URL(`/api/emails/${email.id}`, window.location.origin)
|
||||
if (messageType === 'sent') {
|
||||
url.searchParams.set('type', 'sent')
|
||||
}
|
||||
if (cursor) {
|
||||
url.searchParams.set('cursor', cursor)
|
||||
}
|
||||
@@ -133,7 +142,7 @@ export function MessageList({ email, onMessageSelect, selectedMessageId }: Messa
|
||||
|
||||
const handleDelete = async (message: Message) => {
|
||||
try {
|
||||
const response = await fetch(`/api/emails/${email.id}/${message.id}`, {
|
||||
const response = await fetch(`/api/emails/${email.id}/${message.id}${messageType === 'sent' ? '?type=sent' : ''}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
|
||||
@@ -184,6 +193,14 @@ export function MessageList({ email, onMessageSelect, selectedMessageId }: Messa
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [email.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (refreshTrigger && refreshTrigger > 0) {
|
||||
setRefreshing(true)
|
||||
fetchMessages()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [refreshTrigger])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full flex flex-col">
|
||||
@@ -210,7 +227,7 @@ export function MessageList({ email, onMessageSelect, selectedMessageId }: Messa
|
||||
{messages.map(message => (
|
||||
<div
|
||||
key={message.id}
|
||||
onClick={() => onMessageSelect(message.id)}
|
||||
onClick={() => onMessageSelect(message.id, messageType)}
|
||||
className={cn(
|
||||
"p-3 hover:bg-primary/5 cursor-pointer group",
|
||||
selectedMessageId === message.id && "bg-primary/10"
|
||||
@@ -221,10 +238,12 @@ export function MessageList({ email, onMessageSelect, selectedMessageId }: Messa
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="font-medium text-sm truncate">{message.subject}</p>
|
||||
<div className="mt-1 flex items-center gap-2 text-xs text-gray-500">
|
||||
<span className="truncate">{message.from_address}</span>
|
||||
<span className="truncate">
|
||||
{message.from_address || message.to_address || ''}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{new Date(message.received_at).toLocaleString()}
|
||||
{new Date(message.received_at || message.sent_at || 0).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +269,7 @@ export function MessageList({ email, onMessageSelect, selectedMessageId }: Messa
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-4 text-center text-sm text-gray-500">
|
||||
暂无邮件
|
||||
{messageType === 'sent' ? '暂无发送的邮件' : '暂无收到的邮件'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,41 +5,72 @@ import { Loader2 } from "lucide-react"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
from_address: string
|
||||
from_address?: string
|
||||
to_address?: string
|
||||
subject: string
|
||||
content: string
|
||||
html: string | null
|
||||
received_at: number
|
||||
html?: string
|
||||
received_at?: number
|
||||
sent_at?: number
|
||||
}
|
||||
|
||||
interface MessageViewProps {
|
||||
emailId: string
|
||||
messageId: string
|
||||
messageType?: 'received' | 'sent'
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type ViewMode = "html" | "text"
|
||||
|
||||
export function MessageView({ emailId, messageId }: MessageViewProps) {
|
||||
export function MessageView({ emailId, messageId, messageType = 'received' }: MessageViewProps) {
|
||||
const [message, setMessage] = useState<Message | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [viewMode, setViewMode] = useState<ViewMode>("html")
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const { theme } = useTheme()
|
||||
const { toast } = useToast()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMessage = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/emails/${emailId}/${messageId}`)
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const url = `/api/emails/${emailId}/${messageId}${messageType === 'sent' ? '?type=sent' : ''}`;
|
||||
|
||||
const response = await fetch(url)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
const errorMessage = (errorData as { error?: string }).error || '获取邮件详情失败'
|
||||
setError(errorMessage)
|
||||
toast({
|
||||
title: "错误",
|
||||
description: errorMessage,
|
||||
variant: "destructive"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const data = await response.json() as { message: Message }
|
||||
setMessage(data.message)
|
||||
if (!data.message.html) {
|
||||
setViewMode("text")
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = "网络错误,请稍后重试"
|
||||
setError(errorMessage)
|
||||
toast({
|
||||
title: "错误",
|
||||
description: errorMessage,
|
||||
variant: "destructive"
|
||||
})
|
||||
console.error("Failed to fetch message:", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -47,7 +78,7 @@ export function MessageView({ emailId, messageId }: MessageViewProps) {
|
||||
}
|
||||
|
||||
fetchMessage()
|
||||
}, [emailId, messageId])
|
||||
}, [emailId, messageId, messageType, toast])
|
||||
|
||||
const updateIframeContent = () => {
|
||||
if (viewMode === "html" && message?.html && iframeRef.current) {
|
||||
@@ -151,6 +182,21 @@ export function MessageView({ emailId, messageId }: MessageViewProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<Loader2 className="w-5 h-5 animate-spin text-primary/60" />
|
||||
<span className="ml-2 text-sm text-gray-500">加载邮件详情...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-32 text-center">
|
||||
<p className="text-sm text-destructive mb-2">{error}</p>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
点击重试
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -162,12 +208,17 @@ export function MessageView({ emailId, messageId }: MessageViewProps) {
|
||||
<div className="p-4 space-y-3 border-b border-primary/20">
|
||||
<h3 className="text-base font-bold">{message.subject}</h3>
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
<p>发件人:{message.from_address}</p>
|
||||
<p>时间:{new Date(message.received_at).toLocaleString()}</p>
|
||||
{message.from_address && (
|
||||
<p>发件人:{message.from_address}</p>
|
||||
)}
|
||||
{message.to_address && (
|
||||
<p>收件人:{message.to_address}</p>
|
||||
)}
|
||||
<p>时间:{new Date(message.sent_at || message.received_at || 0).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message.html && (
|
||||
{message.html && message.content && (
|
||||
<div className="border-b border-primary/20 p-2">
|
||||
<RadioGroup
|
||||
value={viewMode}
|
||||
|
||||
138
app/components/emails/send-dialog.tsx
Normal file
138
app/components/emails/send-dialog.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||
import { Send } from "lucide-react"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
interface SendDialogProps {
|
||||
emailId: string
|
||||
fromAddress: string
|
||||
onSendSuccess?: () => void
|
||||
}
|
||||
|
||||
export function SendDialog({ emailId, fromAddress, onSendSuccess }: SendDialogProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [to, setTo] = useState("")
|
||||
const [subject, setSubject] = useState("")
|
||||
const [content, setContent] = useState("")
|
||||
const { toast } = useToast()
|
||||
|
||||
const handleSend = async () => {
|
||||
if (!to.trim() || !subject.trim() || !content.trim()) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "收件人、主题和内容都是必填项",
|
||||
variant: "destructive"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch(`/api/emails/${emailId}/send`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ to, subject, content })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
toast({
|
||||
title: "错误",
|
||||
description: (data as { error: string }).error,
|
||||
variant: "destructive"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "邮件已发送"
|
||||
})
|
||||
setOpen(false)
|
||||
setTo("")
|
||||
setSubject("")
|
||||
setContent("")
|
||||
|
||||
onSendSuccess?.()
|
||||
|
||||
} catch {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: "发送邮件失败",
|
||||
variant: "destructive"
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<DialogTrigger asChild>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 gap-2 hover:bg-primary/10 hover:text-primary transition-colors"
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">发送邮件</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</DialogTrigger>
|
||||
<TooltipContent className="sm:hidden">
|
||||
<p>使用此邮箱发送新邮件</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>发送新邮件</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
发件人: {fromAddress}
|
||||
</div>
|
||||
<Input
|
||||
value={to}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTo(e.target.value)}
|
||||
placeholder="收件人邮箱地址"
|
||||
/>
|
||||
<Input
|
||||
value={subject}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSubject(e.target.value)}
|
||||
placeholder="邮件主题"
|
||||
/>
|
||||
<Textarea
|
||||
value={content}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setContent(e.target.value)}
|
||||
placeholder="邮件内容"
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setOpen(false)} disabled={loading}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={handleSend} disabled={loading}>
|
||||
{loading ? "发送中..." : "发送"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import { EmailList } from "./email-list"
|
||||
import { MessageList } from "./message-list"
|
||||
import { MessageListContainer } from "./message-list-container"
|
||||
import { MessageView } from "./message-view"
|
||||
import { SendDialog } from "./send-dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useCopy } from "@/hooks/use-copy"
|
||||
import { useSendPermission } from "@/hooks/use-send-permission"
|
||||
import { Copy } from "lucide-react"
|
||||
|
||||
interface Email {
|
||||
@@ -16,7 +18,10 @@ interface Email {
|
||||
export function ThreeColumnLayout() {
|
||||
const [selectedEmail, setSelectedEmail] = useState<Email | null>(null)
|
||||
const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null)
|
||||
const [selectedMessageType, setSelectedMessageType] = useState<'received' | 'sent'>('received')
|
||||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||||
const { copyToClipboard } = useCopy()
|
||||
const { canSend: canSendEmails } = useSendPermission()
|
||||
|
||||
const columnClass = "border-2 border-primary/20 bg-background rounded-lg overflow-hidden flex flex-col"
|
||||
const headerClass = "p-2 border-b-2 border-primary/20 flex items-center justify-between shrink-0"
|
||||
@@ -35,6 +40,15 @@ export function ThreeColumnLayout() {
|
||||
copyToClipboard(selectedEmail?.address || "")
|
||||
}
|
||||
|
||||
const handleMessageSelect = (messageId: string | null, messageType: 'received' | 'sent' = 'received') => {
|
||||
setSelectedMessageId(messageId)
|
||||
setSelectedMessageType(messageType)
|
||||
}
|
||||
|
||||
const handleSendSuccess = () => {
|
||||
setRefreshTrigger(prev => prev + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-5 pt-20 h-full flex flex-col">
|
||||
{/* 桌面端三栏布局 */}
|
||||
@@ -58,11 +72,20 @@ export function ThreeColumnLayout() {
|
||||
<div className={headerClass}>
|
||||
<h2 className={titleClass}>
|
||||
{selectedEmail ? (
|
||||
<div className="w-full flex items-center gap-2">
|
||||
<span className="truncate min-w-0">{selectedEmail.address}</span>
|
||||
<div className="shrink-0 cursor-pointer text-primary" onClick={copyEmailAddress}>
|
||||
<Copy className="size-4" />
|
||||
<div className="w-full flex justify-between items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate min-w-0">{selectedEmail.address}</span>
|
||||
<div className="shrink-0 cursor-pointer text-primary" onClick={copyEmailAddress}>
|
||||
<Copy className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
{selectedEmail && canSendEmails && (
|
||||
<SendDialog
|
||||
emailId={selectedEmail.id}
|
||||
fromAddress={selectedEmail.address}
|
||||
onSendSuccess={handleSendSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
"选择邮箱查看消息"
|
||||
@@ -71,10 +94,11 @@ export function ThreeColumnLayout() {
|
||||
</div>
|
||||
{selectedEmail && (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<MessageList
|
||||
<MessageListContainer
|
||||
email={selectedEmail}
|
||||
onMessageSelect={setSelectedMessageId}
|
||||
onMessageSelect={handleMessageSelect}
|
||||
selectedMessageId={selectedMessageId}
|
||||
refreshTrigger={refreshTrigger}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -91,6 +115,7 @@ export function ThreeColumnLayout() {
|
||||
<MessageView
|
||||
emailId={selectedEmail.id}
|
||||
messageId={selectedMessageId}
|
||||
messageType={selectedMessageType}
|
||||
onClose={() => setSelectedMessageId(null)}
|
||||
/>
|
||||
</div>
|
||||
@@ -128,18 +153,28 @@ export function ThreeColumnLayout() {
|
||||
>
|
||||
← 返回邮箱列表
|
||||
</button>
|
||||
<div className="flex-1 flex items-center gap-2 min-w-0">
|
||||
<span className="truncate min-w-0 flex-1 text-right">{selectedEmail.address}</span>
|
||||
<div className="shrink-0 cursor-pointer text-primary" onClick={copyEmailAddress}>
|
||||
<Copy className="size-4" />
|
||||
<div className="flex-1 flex justify-between items-center gap-2 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate min-w-0 flex-1 text-right">{selectedEmail.address}</span>
|
||||
<div className="shrink-0 cursor-pointer text-primary" onClick={copyEmailAddress}>
|
||||
<Copy className="size-4" />
|
||||
</div>
|
||||
</div>
|
||||
{canSendEmails && (
|
||||
<SendDialog
|
||||
emailId={selectedEmail.id}
|
||||
fromAddress={selectedEmail.address}
|
||||
onSendSuccess={handleSendSuccess}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
<MessageList
|
||||
<MessageListContainer
|
||||
email={selectedEmail}
|
||||
onMessageSelect={setSelectedMessageId}
|
||||
onMessageSelect={handleMessageSelect}
|
||||
selectedMessageId={selectedMessageId}
|
||||
refreshTrigger={refreshTrigger}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,6 +195,7 @@ export function ThreeColumnLayout() {
|
||||
<MessageView
|
||||
emailId={selectedEmail.id}
|
||||
messageId={selectedMessageId}
|
||||
messageType={selectedMessageType}
|
||||
onClose={() => setSelectedMessageId(null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user