Skip to content
File

Blob: src/client/components/inbox-view.tsx

typescript133 lines
1import { useCallback } from "react";
2import { ArrowLeft } from "lucide-react";
3import { EmailDetail } from "@/client/components/email-detail";
4import { EmailList } from "@/client/components/email-list";
5import { InboxHeader } from "@/client/components/inbox/inbox-header";
6import { MissingInboxSessionState } from "@/client/components/inbox/missing-inbox-session-state";
7import { ErrorBanner } from "@/client/components/ui";
8import { useCountdown } from "@/client/hooks/useCountdown";
9import { useInbox } from "@/client/hooks/useInbox";
10import { useInboxPageActions } from "@/client/hooks/useInboxPageActions";
11import { useInboxRouteSession } from "@/client/hooks/useInboxRouteSession";
12import { useWebSocket } from "@/client/hooks/useWebSocket";
13import { NewEmailEvent } from "@/shared/contracts";
14 
15interface InboxViewProps {
16 onDeleted: (address: string) => void;
17}
18 
19export 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}