src/mail/Reader.tsx
import type { KeyboardEvent, MouseEvent } from "react";
import type { Draft, GmailLabel, Mail, Provider, State } from "../../shared/types";
import type { Api } from "../api";
import { customLabels } from "../mail-ui";
import { Address, MailPrivacy, Redact } from "../ui/Privacy";
import { Mail as MailIcon } from "../ui/icons";
import { Avatar } from "./Avatar";
import { ConversationEvidence } from "./ConversationEvidence";
import { OfferTimes } from "../work/OfferTimes";
import { InvitationPanel } from "../work/InvitationActions";
import { ReaderContent } from "./MessageContent";
// Today's mail shows its time, this year's its day and time, older mail the full date.
function readerDate(date: number) {
const value = new Date(date);
const now = new Date();
const time: Intl.DateTimeFormatOptions = { hour: "numeric", minute: "2-digit" };
if (value.toDateString() === now.toDateString()) return value.toLocaleTimeString(undefined, time);
return value.toLocaleString(undefined, {
month: "short", day: "numeric", ...(value.getFullYear() === now.getFullYear() ? time : { year: "numeric" }),
});
}
/**
* An opened email: subject, then one sender line with the recipient beneath
* and the date at the right, Gmail labels, and the body with its image consent.
*/
export function Reader({ mail, provider, labels, busy, api, onLabels, onMenu, onMenuKey, onOpen, onChanged, onReplyAll, onOpenDraft, state }: {
mail: Mail;
state?: State;
provider?: Provider;
labels: GmailLabel[];
busy: boolean;
api: Api;
onLabels: () => void;
onOpen?: (mail: Mail) => void;
onChanged?: () => Promise<unknown>;
onReplyAll?: () => void;
onOpenDraft?: (draft: Draft) => void;
onMenu: (event: MouseEvent<HTMLElement>, mail: Mail) => void;
onMenuKey: (event: KeyboardEvent<HTMLElement>, mail: Mail) => void;
}) {
return (
<MailPrivacy mail={mail}>
<article className="message" tabIndex={-1} onContextMenu={(event) => onMenu(event, mail)} onKeyDown={(event) => onMenuKey(event, mail)}>
<h1><Redact text={mail.subject || "(no subject)"} /></h1>
<div className="message-from">
<Avatar mail={mail} provider={provider} />
<div className="from-text">
<p className="from-line">
<strong><Redact text={mail.sender} /></strong>
<span className="from-address"><Address value={mail.address} /></span>
</p>
<p className="from-to">To <Redact text={mail.to} /></p>
{mail.cc && <p className="from-to">Cc <Redact text={mail.cc}/></p>}
</div>
<time dateTime={new Date(mail.date).toISOString()} title={new Date(mail.date).toLocaleString()}>
{readerDate(mail.date)}
</time>
</div>
{provider === "gmail" && (
<div className="message-labels">
<span>
<Redact text={customLabels(mail, labels).filter((label) => mail.labelIds?.includes(label.id)).map((label) => label.name).join(", ") || "No labels"} />
</span>
<button className="link" disabled={busy} onClick={onLabels}>Edit labels</button>
</div>
)}
{onReplyAll && <div className="message-labels"><button type="button" className="link" disabled={busy} onClick={onReplyAll}>Reply all</button></div>}
{mail.snoozedUntil !== null && mail.snoozedUntil > Date.now() && <p className="message-return">Returns to Inbox <time dateTime={new Date(mail.snoozedUntil).toISOString()}>{new Date(mail.snoozedUntil).toLocaleString()}</time> (local time)</p>}
{state && onOpenDraft && <OfferTimes key={`offers:${mail.id}`} mail={mail} state={state} api={api} onOpenDraft={onOpenDraft} />}
<InvitationPanel key={`invitation:${mail.id}`} mail={mail} api={api} onOpenDraft={onOpenDraft} onChanged={onChanged} />
<ReaderContent key={`content:${mail.id}`} mail={mail}
loadContent={(id) => api(`/messages/${encodeURIComponent(id)}/content`, "POST")}
loadImages={(id) => api(`/messages/${encodeURIComponent(id)}/images`, "POST")} />
{onOpen && onChanged && <ConversationEvidence key={`conversation:${mail.id}`} mail={mail} gmail={provider === "gmail"} api={api} onOpen={onOpen} onChanged={onChanged}/>}
</article>
</MailPrivacy>
);
}
/** The reading pane before an email is opened. */
export function ReaderEmpty({ selected, openKey }: { selected: boolean; openKey: string }) {
return (
<div className="reader-empty">
<MailIcon color="currentColor" size={28} />
<p className="empty-title">No email open</p>
<p>{selected ? <>Press <kbd>{openKey}</kbd> to open the selected email.</> : "Choose an email to read it here."}</p>
</div>
);
}