Merge pull request #190 from Coffey-Labs/fix/print-single-message
Print the message you asked to print
This commit is contained in:
@@ -1235,6 +1235,15 @@ select optgroup { background-color: var(--bg-elev); color: var(--fg); }
|
|||||||
*/
|
*/
|
||||||
.message { break-inside: auto; border: 1px solid var(--border); }
|
.message { break-inside: auto; border: 1px solid var(--border); }
|
||||||
.message-head { break-inside: avoid; break-after: avoid; }
|
.message-head { break-inside: avoid; break-after: avoid; }
|
||||||
|
/*
|
||||||
|
* "Print" in a message's own menu marks that card and prints it alone;
|
||||||
|
* "Print conversation" in the thread toolbar sets no mark and prints them
|
||||||
|
* all. The subject heading stays either way -- a printed
|
||||||
|
* message with no subject on it is a page nobody can file -- but the
|
||||||
|
* "3 messages" count beside it goes, since only one of them is on the page.
|
||||||
|
*/
|
||||||
|
.printing-one .message:not(.print-target) { display: none !important; }
|
||||||
|
.printing-one .thread-subject > .muted { display: none !important; }
|
||||||
.print-only { display: block; }
|
.print-only { display: block; }
|
||||||
body { background: #fff; color: #000; }
|
body { background: #fff; color: #000; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
|||||||
const settings = useSettings((s) => s.settings);
|
const settings = useSettings((s) => s.settings);
|
||||||
const updateSettings = useSettings((s) => s.update);
|
const updateSettings = useSettings((s) => s.update);
|
||||||
const reply = useCompose((s) => s.reply);
|
const reply = useCompose((s) => s.reply);
|
||||||
|
const cardRef = useRef<HTMLElement>(null);
|
||||||
const [details, setDetails] = useState(false);
|
const [details, setDetails] = useState(false);
|
||||||
const [showSource, setShowSource] = useState(false);
|
const [showSource, setShowSource] = useState(false);
|
||||||
const [showHeaders, setShowHeaders] = useState(false);
|
const [showHeaders, setShowHeaders] = useState(false);
|
||||||
@@ -146,11 +147,47 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
|||||||
if (!expanded) onToggle();
|
if (!expanded) onToggle();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Print this message, not the conversation it happens to sit in.
|
||||||
|
*
|
||||||
|
* The card is inside the thread, so a bare window.print() prints every
|
||||||
|
* message on the page -- which is what the toolbar's "Print conversation"
|
||||||
|
* is for, and not what someone asks for from a single message's menu.
|
||||||
|
* The two marker classes let the print stylesheet drop the siblings for
|
||||||
|
* the duration; the subject heading stays, since a printed message with no
|
||||||
|
* subject on it is a page nobody can file.
|
||||||
|
*
|
||||||
|
* window.print() blocks until the dialog is dismissed, so clearing the
|
||||||
|
* marks after it returns is enough on its own; `afterprint` is there for a
|
||||||
|
* browser that ever makes it asynchronous, and running twice is harmless.
|
||||||
|
*/
|
||||||
|
const printThis = () => {
|
||||||
|
const card = cardRef.current;
|
||||||
|
if (!card) {
|
||||||
|
window.print();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = document.documentElement;
|
||||||
|
const clear = () => {
|
||||||
|
root.classList.remove("printing-one");
|
||||||
|
card.classList.remove("print-target");
|
||||||
|
window.removeEventListener("afterprint", clear);
|
||||||
|
};
|
||||||
|
window.addEventListener("afterprint", clear);
|
||||||
|
root.classList.add("printing-one");
|
||||||
|
card.classList.add("print-target");
|
||||||
|
try {
|
||||||
|
window.print();
|
||||||
|
} finally {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
/* `wasUnread` rather than `$seen`: the bar marks what was unread when the
|
/* `wasUnread` rather than `$seen`: the bar marks what was unread when the
|
||||||
conversation was opened, and keeps marking it after the auto-mark-read
|
conversation was opened, and keeps marking it after the auto-mark-read
|
||||||
timer has told the server otherwise. Losing it mid-read was half of #69. */
|
timer has told the server otherwise. Losing it mid-read was half of #69. */
|
||||||
<article className={`message ${expanded ? "" : "collapsed"} ${wasUnread ?? !e.keywords.$seen ? "unread-msg" : ""}`} data-msg-id={e.id} onClick={collapsedClick}>
|
<article ref={cardRef} className={`message ${expanded ? "" : "collapsed"} ${wasUnread ?? !e.keywords.$seen ? "unread-msg" : ""}`} data-msg-id={e.id} onClick={collapsedClick}>
|
||||||
<header className="message-head" onClick={(ev) => { if (expanded && !(ev.target as HTMLElement).closest("button,a,.message-details")) onToggle(); }}>
|
<header className="message-head" onClick={(ev) => { if (expanded && !(ev.target as HTMLElement).closest("button,a,.message-details")) onToggle(); }}>
|
||||||
<Avatar who={from ?? null} />
|
<Avatar who={from ?? null} />
|
||||||
<div className="who">
|
<div className="who">
|
||||||
@@ -200,7 +237,7 @@ export const MessageView = memo(function MessageView({ email: e, expanded, wasUn
|
|||||||
<MenuItem icon={<Eye size={16} />} label={translate("Show original")} onClick={() => void openSource()} />
|
<MenuItem icon={<Eye size={16} />} label={translate("Show original")} onClick={() => void openSource()} />
|
||||||
<MenuItem icon={<Code size={16} />} label={translate("Show headers")} onClick={() => setShowHeaders(true)} />
|
<MenuItem icon={<Code size={16} />} label={translate("Show headers")} onClick={() => setShowHeaders(true)} />
|
||||||
<MenuItem icon={<Download size={16} />} label={translate("Download (.eml)")} onClick={downloadEml} />
|
<MenuItem icon={<Download size={16} />} label={translate("Download (.eml)")} onClick={downloadEml} />
|
||||||
<MenuItem icon={<Printer size={16} />} label={translate("Print")} onClick={() => window.print()} />
|
<MenuItem icon={<Printer size={16} />} label={translate("Print")} onClick={printThis} />
|
||||||
<MenuItem icon={<Filter size={16} />} label={translate("Filter messages like this…")} onClick={() => setFilterOpen(true)} />
|
<MenuItem icon={<Filter size={16} />} label={translate("Filter messages like this…")} onClick={() => setFilterOpen(true)} />
|
||||||
{hasCalendar && <MenuItem icon={<CalendarPlus size={16} />} label={translate("Create event…")} onClick={() => void startAppointment(e, navigate).catch((err: unknown) => toast.error((err as Error).message))} />}
|
{hasCalendar && <MenuItem icon={<CalendarPlus size={16} />} label={translate("Create event…")} onClick={() => void startAppointment(e, navigate).catch((err: unknown) => toast.error((err as Error).message))} />}
|
||||||
{from && (
|
{from && (
|
||||||
|
|||||||
Reference in New Issue
Block a user