This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, Mail, Paperclip } from 'lucide-react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import type { EmailSummary } from 'types';
|
||||
|
||||
const LIMIT = 50;
|
||||
|
||||
const formatDate = (iso: string) => {
|
||||
const date = new Date(iso);
|
||||
const now = new Date();
|
||||
@@ -17,13 +20,17 @@ const formatDate = (iso: string) => {
|
||||
export const EmailList = () => {
|
||||
const client = useClient();
|
||||
const [selectedId, setSelectedId] = useGlobal<string | null>('EMAIL_SELECTED', null);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['email-messages'],
|
||||
queryFn: () => client.get<{ messages: EmailSummary[]; total: number }>('/email/messages'),
|
||||
queryKey: ['email-messages', page],
|
||||
queryFn: () =>
|
||||
client.get<{ messages: EmailSummary[]; total: number }>(`/email/messages?page=${page}&limit=${LIMIT}`),
|
||||
});
|
||||
|
||||
const messages = data?.messages ?? [];
|
||||
const total = data?.total ?? 0;
|
||||
const totalPages = Math.max(1, Math.ceil(total / LIMIT));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -33,7 +40,7 @@ export const EmailList = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (messages.length === 0) {
|
||||
if (messages.length === 0 && page === 1) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2 text-sm opacity-50">
|
||||
<Mail className="h-8 w-8" />
|
||||
@@ -47,14 +54,35 @@ export const EmailList = () => {
|
||||
<div className="flex items-center gap-2 border-b px-3 py-2">
|
||||
<Mail className="h-4 w-4 opacity-60" />
|
||||
<span className="text-sm font-medium">Inbox</span>
|
||||
<span className="text-xs opacity-50">{data?.total ?? 0}</span>
|
||||
<span className="text-xs opacity-50">{total}</span>
|
||||
{totalPages > 1 && (
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page <= 1}
|
||||
className="flex items-center text-xs disabled:opacity-30 cursor-pointer disabled:cursor-default"
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<span className="text-xs opacity-60">
|
||||
{page}/{totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page >= totalPages}
|
||||
className="flex items-center text-xs disabled:opacity-30 cursor-pointer disabled:cursor-default"
|
||||
>
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-1 flex-col divide-y divide-white/10">
|
||||
{messages.map((msg: EmailSummary) => (
|
||||
<button
|
||||
key={msg.id}
|
||||
onClick={() => setSelectedId(msg.id)}
|
||||
className={`flex flex-col gap-0.5 border-b px-3 py-2.5 text-left transition-colors cursor-pointer ${
|
||||
className={`flex flex-col gap-0.5 px-3 py-2.5 text-left transition-colors cursor-pointer ${
|
||||
selectedId === msg.id ? 'bg-accent' : 'hover:bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
@@ -63,7 +91,15 @@ export const EmailList = () => {
|
||||
<span className="shrink-0 text-xs opacity-50">{formatDate(msg.date)}</span>
|
||||
</div>
|
||||
<span className="truncate text-sm">{msg.subject}</span>
|
||||
<span className="truncate text-xs opacity-50">{msg.snippet}</span>
|
||||
<div className="flex items-center gap-1.5 text-xs">
|
||||
{!!msg.attachmentCount && (
|
||||
<span className="flex shrink-0 items-center gap-0.5 text-muted-foreground">
|
||||
<Paperclip className="h-3 w-3" />
|
||||
{msg.attachmentCount}
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate opacity-50">{msg.snippet}</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Mail } from 'lucide-react';
|
||||
import { FileViewerProvider, FileViewerHeader, FileViewerBody } from 'officerdev';
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { useGlobal } from 'hooks/useGlobal';
|
||||
import type { EmailMessage } from 'types';
|
||||
|
||||
type OpenAttachment = {
|
||||
filePath: string;
|
||||
fileName: string;
|
||||
root: string;
|
||||
};
|
||||
|
||||
const HtmlBody = ({ html }: { html: string }) => {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
@@ -36,6 +44,7 @@ const HtmlBody = ({ html }: { html: string }) => {
|
||||
export const EmailReader = () => {
|
||||
const client = useClient();
|
||||
const [selectedId] = useGlobal<string | null>('EMAIL_SELECTED', null);
|
||||
const [openAttachment, setOpenAttachment] = useState<OpenAttachment | null>(null);
|
||||
|
||||
const { data: message, isLoading } = useQuery({
|
||||
queryKey: ['email-message', selectedId],
|
||||
@@ -43,6 +52,12 @@ export const EmailReader = () => {
|
||||
enabled: !!selectedId,
|
||||
});
|
||||
|
||||
const extractAttachment = async (index: number) => {
|
||||
if (!selectedId) return;
|
||||
const result = await client.post<OpenAttachment>(`/email/messages/${selectedId}/attachments/${index}/extract`);
|
||||
setOpenAttachment(result);
|
||||
};
|
||||
|
||||
if (!selectedId) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-2 text-sm opacity-50">
|
||||
@@ -83,9 +98,13 @@ export const EmailReader = () => {
|
||||
{message.attachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-1">
|
||||
{message.attachments.map((att: { filename: string; size: number; contentType: string }, i: number) => (
|
||||
<span key={i} className="rounded bg-accent px-2 py-0.5 text-xs">
|
||||
<button
|
||||
key={i}
|
||||
className="cursor-pointer rounded bg-accent px-2 py-0.5 text-xs hover:bg-accent/80"
|
||||
onClick={() => extractAttachment(i)}
|
||||
>
|
||||
{att.filename} ({Math.round(att.size / 1024)}KB)
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -97,6 +116,26 @@ export const EmailReader = () => {
|
||||
<pre className="whitespace-pre-wrap p-4 text-sm">{message.text}</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Dialog open={!!openAttachment} onOpenChange={(open) => !open && setOpenAttachment(null)}>
|
||||
<DialogContent className="flex h-[80vh] max-w-4xl flex-col gap-0 p-0">
|
||||
<DialogTitle className="sr-only">{openAttachment?.fileName}</DialogTitle>
|
||||
{openAttachment && (
|
||||
<FileViewerProvider
|
||||
filePath={openAttachment.filePath}
|
||||
fileName={openAttachment.fileName}
|
||||
root={openAttachment.root}
|
||||
>
|
||||
<div className="flex shrink-0 items-center gap-2 border-b px-4 py-1.5">
|
||||
<FileViewerHeader />
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
<FileViewerBody />
|
||||
</div>
|
||||
</FileViewerProvider>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user