File
Blob: src/client/components/inbox-view.tsx
| 1 | import { useCallback } from "react"; |
| 2 | import { ArrowLeft } from "lucide-react"; |
| 3 | import { EmailDetail } from "@/client/components/email-detail"; |
| 4 | import { EmailList } from "@/client/components/email-list"; |
| 5 | import { InboxHeader } from "@/client/components/inbox/inbox-header"; |
| 6 | import { MissingInboxSessionState } from "@/client/components/inbox/missing-inbox-session-state"; |
| 7 | import { ErrorBanner } from "@/client/components/ui"; |
| 8 | import { useCountdown } from "@/client/hooks/useCountdown"; |
| 9 | import { useInbox } from "@/client/hooks/useInbox"; |
| 10 | import { useInboxPageActions } from "@/client/hooks/useInboxPageActions"; |
| 11 | import { useInboxRouteSession } from "@/client/hooks/useInboxRouteSession"; |
| 12 | import { useWebSocket } from "@/client/hooks/useWebSocket"; |
| 13 | import { NewEmailEvent } from "@/shared/contracts"; |
| 14 | |
| 15 | interface InboxViewProps { |
| 16 | onDeleted: (address: string) => void; |
| 17 | } |
| 18 | |
| 19 | export function InboxView({ onDeleted }: InboxViewProps) { |
| 20 | const { address, adminMode, auth } = useInboxRouteSession(); |
| 21 | const session = auth ? { address, auth } : null; |
| 22 | const { |
| 23 | inbox, |
| 24 | emails, |
| 25 | selectedEmail, |
| 26 | selectedEmailId, |
| 27 | loading, |
| 28 | emailLoading, |
| 29 | error, |
| 30 | refreshInbox, |
| 31 | refresh, |
| 32 | selectEmail, |
| 33 | deleteEmail: removeEmail, |
| 34 | applyIncomingEmail, |
| 35 | } = useInbox(session, { markReadOnOpen: !adminMode }); |
| 36 | |
| 37 | const countdown = useCountdown(inbox?.expiresAt ?? null); |
| 38 | const { |
| 39 | availableExtensions, |
| 40 | canDeleteEmail, |
| 41 | extendingTo, |
| 42 | isAdminInspectingTemporaryInbox, |
| 43 | handleBackToList, |
| 44 | handleCopy, |
| 45 | handleDeleteInbox, |
| 46 | handleExtendInbox, |
| 47 | } = useInboxPageActions({ |
| 48 | address, |
| 49 | adminMode, |
| 50 | inbox, |
| 51 | onDeleted, |
| 52 | refreshInbox, |
| 53 | selectEmail, |
| 54 | session, |
| 55 | }); |
| 56 | |
| 57 | const socketState = useWebSocket<NewEmailEvent>({ |
| 58 | address, |
| 59 | auth, |
| 60 | enabled: Boolean(session), |
| 61 | messageCodec: NewEmailEvent, |
| 62 | onMessage: useCallback( |
| 63 | (message: NewEmailEvent) => { |
| 64 | if (message.type === "new_email") { |
| 65 | void applyIncomingEmail(message.email); |
| 66 | } |
| 67 | }, |
| 68 | [applyIncomingEmail], |
| 69 | ), |
| 70 | }); |
| 71 | |
| 72 | if (!session) { |
| 73 | return <MissingInboxSessionState />; |
| 74 | } |
| 75 | |
| 76 | return ( |
| 77 | <main className="animate-slide-up space-y-5"> |
| 78 | {/* Inbox hero bar */} |
| 79 | <InboxHeader |
| 80 | address={address} |
| 81 | adminMode={adminMode} |
| 82 | availableExtensions={availableExtensions} |
| 83 | countdown={countdown} |
| 84 | extendingTo={extendingTo} |
| 85 | inbox={inbox} |
| 86 | isAdminInspectingTemporaryInbox={isAdminInspectingTemporaryInbox} |
| 87 | socketState={socketState} |
| 88 | onCopy={handleCopy} |
| 89 | onDeleteInbox={handleDeleteInbox} |
| 90 | onExtendInbox={handleExtendInbox} |
| 91 | onRefresh={refresh} |
| 92 | /> |
| 93 | |
| 94 | {error ? <ErrorBanner>{error}</ErrorBanner> : null} |
| 95 | |
| 96 | {/* Email list + detail — desktop: side-by-side, mobile: toggle */} |
| 97 | <div className="grid gap-5 lg:grid-cols-[minmax(280px,400px)_minmax(0,1fr)]"> |
| 98 | <div className={selectedEmailId ? "hidden lg:block" : ""}> |
| 99 | <EmailList |
| 100 | inboxAddress={address} |
| 101 | emails={emails} |
| 102 | selectedEmailId={selectedEmailId} |
| 103 | loadingEmailId={emailLoading ? selectedEmailId : null} |
| 104 | loading={loading} |
| 105 | onSelect={(emailId) => void selectEmail(emailId)} |
| 106 | /> |
| 107 | </div> |
| 108 | <div className={selectedEmailId ? "" : "hidden lg:block"}> |
| 109 | {selectedEmailId ? ( |
| 110 | <button |
| 111 | type="button" |
| 112 | className="mb-3 flex items-center gap-1.5 text-xs font-medium text-zinc-400 transition-colors hover:text-zinc-200 lg:hidden" |
| 113 | onClick={handleBackToList} |
| 114 | > |
| 115 | <ArrowLeft className="h-3.5 w-3.5" /> |
| 116 | Back to inbox |
| 117 | </button> |
| 118 | ) : null} |
| 119 | <EmailDetail |
| 120 | address={address} |
| 121 | auth={session.auth} |
| 122 | email={selectedEmail} |
| 123 | loading={emailLoading} |
| 124 | canDelete={canDeleteEmail} |
| 125 | canViewRaw={adminMode} |
| 126 | onDelete={(emailId) => void removeEmail(emailId)} |
| 127 | /> |
| 128 | </div> |
| 129 | </div> |
| 130 | </main> |
| 131 | ); |
| 132 | } |