Use the full width for message bodies on mobile (#112)
* Use the full width for message bodies on mobile The reader indents the subject, sender row and body by the avatar column (pl-12/pl-16) at every width, which leaves a 64px gutter on a phone. Below the sm breakpoint the subject, body, attachments and the scheduled-send banner's wrapper now start at the page edge; wider screens are unchanged. * improve mobile layout for email content --------- Co-authored-by: Hieu Nguyen (Jack) <nc.hieu@live.com>
This commit is contained in:
1 parent
b24535647c
commit
f0fddecd2e
4 files changed
+55
-28
No files matched your search
@@ -54,6 +54,7 @@ export default function MessageDetailPage() {
|
||||
const [previewAttachment, setPreviewAttachment] =
|
||||
useState<MessageAttachment | null>(null);
|
||||
const [threadExpanded, setThreadExpanded] = useState(false);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [latestMessagesFirst] = useLatestMessagesFirst();
|
||||
const { enabled: showFullRecipientAddresses } = useShowFullRecipientAddresses();
|
||||
const { visible: messageListVisible } = useMessageListVisibility();
|
||||
@@ -96,6 +97,7 @@ export default function MessageDetailPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setThreadExpanded(false);
|
||||
setDetailsOpen(false);
|
||||
}, [messageId]);
|
||||
|
||||
if (loading) {
|
||||
@@ -149,6 +151,11 @@ export default function MessageDetailPage() {
|
||||
const cloudAttachmentResult = extractCloudAttachments(
|
||||
bodyDisplay.latestContent,
|
||||
);
|
||||
const messageDate = (withTime: boolean) => scheduledAt
|
||||
? formatScheduledSendTime(scheduledAt, t)
|
||||
: formatUserDate(message.createdAt, withTime
|
||||
? { month: "short", day: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" }
|
||||
: { month: "short", day: "2-digit", year: "numeric" });
|
||||
return (<>
|
||||
|
||||
<div className={clsx("flex py-2 h-12 items-center px-2 border-b sticky top-0 bg-white z-40 gap-3", scrolled ? "border-neutral-200" : "border-transparent")}>
|
||||
@@ -184,7 +191,7 @@ export default function MessageDetailPage() {
|
||||
<div ref={scrollRef} onScroll={handleScroll} className="h-full overflow-y-auto overscroll-contain scrollbar-gutter-stable flex-1 min-h-0">
|
||||
{!message.read && <MarkAsRead messageId={message.id} />}
|
||||
|
||||
<h1 className={clsx(!isAnyPanelVisible ? "pl-16" : "pl-10", "pr-6 pb-2 pt-2 text-2xl text-neutral-900")} title={message.subject ?? t("list.noSubject")}>
|
||||
<h1 className={clsx(!isAnyPanelVisible ? "sm:pl-16" : "sm:pl-10", "pl-3 pr-3 pb-2 pt-2 text-2xl text-neutral-900 sm:pr-6")} title={message.subject ?? t("list.noSubject")}>
|
||||
{message.subject ?? t("list.noSubject")}
|
||||
</h1>
|
||||
{/* <div className="px-6">
|
||||
@@ -210,9 +217,9 @@ export default function MessageDetailPage() {
|
||||
expandedAll={threadExpanded}
|
||||
onExpandedAllChange={setThreadExpanded}
|
||||
/>
|
||||
<article className={clsx("px-6 py-4", !isAnyPanelVisible && "pl-12")}>
|
||||
<article className={clsx("px-3 py-4 sm:px-6", !isAnyPanelVisible && "sm:pl-12")}>
|
||||
<div className="w-full">
|
||||
<div className="flex flex-wrap items-start justify-between gap-x-3 gap-y-1 pb-5 pl-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-x-3 gap-y-1 pb-5 sm:pl-4">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<ContactAvatar
|
||||
mailboxId={message.mailboxId}
|
||||
@@ -237,7 +244,7 @@ export default function MessageDetailPage() {
|
||||
fromName
|
||||
)}
|
||||
</b>{" "}
|
||||
<span className="text-neutral-500 text-xs flex-1 overflow-hidden text-ellipsis min-w-0"><{fromAddress}></span>
|
||||
<span className="hidden sm:block text-neutral-500 text-xs flex-1 overflow-hidden text-ellipsis min-w-0"><{fromAddress}></span>
|
||||
</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
{t("source.to").toLowerCase()}{" "}
|
||||
@@ -247,6 +254,12 @@ export default function MessageDetailPage() {
|
||||
style={showFullRecipientAddresses ? "full" : "address"}
|
||||
/>
|
||||
</p>
|
||||
<div className="sm:hidden flex items-center gap-2 text-xs text-neutral-500">
|
||||
<span>{messageDate(false)}</span>
|
||||
<button type="button" className="text-blue-600 hover:underline" aria-expanded={detailsOpen} onClick={() => setDetailsOpen((open) => !open)}>
|
||||
{t("message.details")}
|
||||
</button>
|
||||
</div>
|
||||
{ccEntries.length > 0 && (
|
||||
<p className="text-xs text-neutral-500">
|
||||
{t("message.cc")} <RecipientList entries={ccEntries} mailboxId={message.mailboxId} style={showFullRecipientAddresses ? "full" : "address"} />
|
||||
@@ -260,11 +273,7 @@ export default function MessageDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<p className="text-xs">
|
||||
{scheduledAt
|
||||
? formatScheduledSendTime(scheduledAt, t)
|
||||
: formatUserDate(message.createdAt, { month: "short", day: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}
|
||||
</p>
|
||||
<p className="hidden text-xs sm:block">{messageDate(true)}</p>
|
||||
<ThreadMessageActions
|
||||
message={currentThreadMessage}
|
||||
mailboxId={message.mailboxId}
|
||||
@@ -273,9 +282,16 @@ export default function MessageDetailPage() {
|
||||
starOnly={!thread.messages.some((threadMessage) => threadMessage.id === message.id) || !thread.messages.some((threadMessage) => threadMessage.id !== message.id)}
|
||||
/>
|
||||
</div>
|
||||
{detailsOpen && (
|
||||
<dl className="sm:hidden w-full space-y-0.5 break-words pt-2 text-xs text-neutral-500">
|
||||
<div><dt className="inline font-medium">{t("source.from")}:</dt> <dd className="inline">{fromName} <{fromAddress}></dd></div>
|
||||
<div><dt className="inline font-medium">{t("source.to")}:</dt> <dd className="inline">{toEntries.join(", ")}</dd></div>
|
||||
<div><dt className="inline font-medium">{t("message.date")}:</dt> <dd className="inline">{messageDate(true)}</dd></div>
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
{scheduledAt && <div className="pl-10"><ScheduledSendBanner messageId={message.id} scheduledAt={scheduledAt} /></div>}
|
||||
<div className="prose max-w-none pl-16 text-neutral-900">
|
||||
{scheduledAt && <div className="sm:pl-10"><ScheduledSendBanner messageId={message.id} scheduledAt={scheduledAt} /></div>}
|
||||
<div className="prose max-w-none text-neutral-900 sm:pl-16">
|
||||
{htmlBody ? (
|
||||
<EmailHtmlRenderer key={message.id} className="mx-auto" html={htmlBody} />
|
||||
) : (
|
||||
@@ -292,7 +308,7 @@ export default function MessageDetailPage() {
|
||||
))}
|
||||
</div>
|
||||
{cloudAttachmentResult.attachments.length > 0 && (
|
||||
<section className="mt-8 border-t border-neutral-100 py-6 pl-16">
|
||||
<section className="mt-8 border-t border-neutral-100 py-6 sm:pl-16">
|
||||
<h2 className="mb-3 text-sm font-semibold text-neutral-900">
|
||||
{t("message.cloudFiles", { count: cloudAttachmentResult.attachments.length })}
|
||||
</h2>
|
||||
@@ -321,7 +337,7 @@ export default function MessageDetailPage() {
|
||||
</section>
|
||||
)}
|
||||
{attachments.length > 0 && (
|
||||
<section className="mt-8 border-t border-neutral-100 py-6 pl-16">
|
||||
<section className="mt-8 border-t border-neutral-100 py-6 sm:pl-16">
|
||||
<h2 className="mb-3 text-sm font-semibold text-neutral-900">
|
||||
{t("message.attachments", { count: attachments.length })}
|
||||
</h2>
|
||||
|
||||
@@ -261,16 +261,6 @@ export function MessageActions({
|
||||
|
||||
{isMobile && (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="roundedSM"
|
||||
aria-label={t("message.reply")}
|
||||
disabled={disabled}
|
||||
onClick={() => handleReply("reply")}
|
||||
>
|
||||
<Reply size={iconSize} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="roundedSM"
|
||||
@@ -408,6 +398,10 @@ export function MessageActions({
|
||||
<Archive className="h-4 w-4" />
|
||||
{t("common.archive")}
|
||||
</button>
|
||||
<button type="button" className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 disabled:text-neutral-400" onClick={() => { setMoreOpen(false); void handleReply("reply"); }}>
|
||||
<Reply className="h-4 w-4" />
|
||||
{t("message.reply")}
|
||||
</button>
|
||||
{canReplyAll && (
|
||||
<button type="button" className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 disabled:text-neutral-400" onClick={() => { setMoreOpen(false); void handleReply("replyAll"); }}>
|
||||
<ReplyAll className="h-4 w-4" />
|
||||
|
||||
@@ -123,6 +123,7 @@ export function ConversationMessageCard({
|
||||
const { t } = useLanguage();
|
||||
const [locallyExpanded, setLocallyExpanded] = useState(defaultExpanded);
|
||||
const [locallyRead, setLocallyRead] = useState(message.read);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const expanded = locallyExpanded;
|
||||
const sender = getConversationSender(message, currentAccountName);
|
||||
const senderEmail = getConversationSenderEmail(message);
|
||||
@@ -148,9 +149,9 @@ export function ConversationMessageCard({
|
||||
|
||||
|
||||
return (
|
||||
<article className={cn("bg-white transition-colors px-6", !expanded && "hover:bg-neutral-50", !isAnyPanelVisible && "pl-12")}>
|
||||
<article className={cn("bg-white transition-colors px-3 sm:px-6", !expanded && "hover:bg-neutral-50", !isAnyPanelVisible && "sm:pl-12")}>
|
||||
<div className="w-full">
|
||||
<div className="flex w-full flex-wrap items-start gap-x-3 py-3 pl-4">
|
||||
<div className="flex w-full flex-wrap items-start gap-x-3 py-3 sm:pl-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -175,7 +176,7 @@ export function ConversationMessageCard({
|
||||
<div className="flex flex-col">
|
||||
<span className={cn("truncate text-sm font-semibold mt-1", locallyRead || outbound ? "text-neutral-900" : "font-semibold text-neutral-900")}>
|
||||
{sender}
|
||||
{expanded && <span className="text-xs ml-1 opacity-50 font-normal"><{senderEmail}></span>}
|
||||
{expanded && <span className="hidden sm:inline text-xs ml-1 opacity-50 font-normal"><{senderEmail}></span>}
|
||||
</span>
|
||||
{expanded && recipients && <span className="text-xs font-normal text-neutral-500">{t("thread.to", { recipients })}</span>}
|
||||
</div>
|
||||
@@ -184,7 +185,7 @@ export function ConversationMessageCard({
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
<span className={clsx(!locallyRead ? "font-semibold" : "", "flex shrink-0 items-center gap-2 text-xs mr-2 mt-2")}>
|
||||
<span className={clsx(!locallyRead ? "font-semibold" : "", "shrink-0 items-center gap-2 text-xs mr-2 mt-2", "hidden sm:flex")}>
|
||||
{attachments.length > 0 && <Paperclip className="h-3.5 w-3.5" aria-label={t("thread.attachmentCount", { count: attachments.length })} />}
|
||||
{formatUserDate(message.createdAt, { month: "short", day: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}
|
||||
</span>
|
||||
@@ -195,9 +196,23 @@ export function ConversationMessageCard({
|
||||
ownAddresses={ownAddresses}
|
||||
starOnly={!expanded}
|
||||
/>
|
||||
<div className={clsx("sm:hidden flex w-full items-center gap-2 pl-12 text-xs", locallyRead ? "text-neutral-500" : "font-semibold text-neutral-700")}>
|
||||
{attachments.length > 0 && <Paperclip className="h-3.5 w-3.5" aria-label={t("thread.attachmentCount", { count: attachments.length })} />}
|
||||
<span>{formatUserDate(message.createdAt, { month: "short", day: "2-digit", year: "numeric" })}</span>
|
||||
<button type="button" className="text-blue-600 hover:underline" aria-expanded={detailsOpen} onClick={() => setDetailsOpen((open) => !open)}>
|
||||
{t("message.details")}
|
||||
</button>
|
||||
</div>
|
||||
{detailsOpen && (
|
||||
<dl className="sm:hidden w-full space-y-0.5 break-words pt-2 text-xs text-neutral-500">
|
||||
<div><dt className="inline font-medium">{t("source.from")}:</dt> <dd className="inline">{sender} <{senderEmail}></dd></div>
|
||||
{recipients && <div><dt className="inline font-medium">{t("source.to")}:</dt> <dd className="inline">{recipients}</dd></div>}
|
||||
<div><dt className="inline font-medium">{t("message.date")}:</dt> <dd className="inline">{formatUserDate(message.createdAt, { month: "short", day: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })}</dd></div>
|
||||
</dl>
|
||||
)}
|
||||
</div>
|
||||
{expanded && body && (
|
||||
<div className="pb-4 pl-16 pt-2">
|
||||
<div className="pb-4 pt-2 sm:pl-16">
|
||||
{body.html ? (
|
||||
<EmailHtmlRenderer html={body.html} />
|
||||
) : (
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
"message.replyAll": "Reply all",
|
||||
"message.forward": "Forward",
|
||||
"message.showOriginal": "Show original",
|
||||
"message.details": "Details",
|
||||
"message.date": "Date",
|
||||
"message.blockContact": "Block contact",
|
||||
"message.star": "Star",
|
||||
"message.removeStar": "Remove star",
|
||||
|
||||
Reference in new issue
Block a user