feat(email-viewer): message spacing setting (auto/always/edge-to-edge)
This commit is contained in:
@@ -654,6 +654,7 @@ export function EmailViewer({
|
||||
const tDemoWelcome = useTranslations('demo_welcome');
|
||||
const tWelcome = useTranslations('welcome');
|
||||
const externalContentPolicy = useSettingsStore((state) => state.externalContentPolicy);
|
||||
const messageSpacing = useSettingsStore((state) => state.messageSpacing);
|
||||
const mailAttachmentAction = useSettingsStore((state) => state.mailAttachmentAction);
|
||||
const attachmentPosition = useSettingsStore((state) => state.attachmentPosition);
|
||||
const addTrustedSender = useSettingsStore((state) => state.addTrustedSender);
|
||||
@@ -2116,9 +2117,21 @@ export function EmailViewer({
|
||||
// Word/Outlook HTML emails ship a <style> block but put their gutter in
|
||||
// @page margins (print-only), so they need a fallback body padding too.
|
||||
const isWordHtml = /class=["']?(?:Mso|WordSection)|<o:p[\s>/]|urn:schemas-microsoft-com:office:office/i.test(effectiveEmailContent.html);
|
||||
const hasOwnLayout = effectiveEmailContent.hasStyleTag && !isWordHtml;
|
||||
const bodyPadding = hasOwnLayout ? '0' : '1rem 1.25rem';
|
||||
const mobileBodyPaddingX = hasOwnLayout ? '0' : '0.75rem';
|
||||
// "auto" spacing: only drop our gutter when the mail paints a full-bleed
|
||||
// background canvas (a width:100% element carrying a background colour) --
|
||||
// the one case where the gutter shows as a frame around the email's own
|
||||
// background. A <style> tag alone is too weak a signal: plenty of
|
||||
// transactional mails ship one for web fonts yet have no gutter of their
|
||||
// own, and zeroing the padding glues their content to the corner.
|
||||
const emailHtml = effectiveEmailContent.html;
|
||||
const hasFullBleedCanvas =
|
||||
/<(?:table|div|body)\b[^>]*(?:\bwidth\s*=\s*["']?\s*100%|width\s*:\s*100%)[^>]*(?:\bbgcolor\s*=|background(?:-color)?\s*:)/i.test(emailHtml) ||
|
||||
/<(?:table|div|body)\b[^>]*(?:\bbgcolor\s*=|background(?:-color)?\s*:)[^>]*(?:\bwidth\s*=\s*["']?\s*100%|width\s*:\s*100%)/i.test(emailHtml);
|
||||
const autoDropsGutter = effectiveEmailContent.hasStyleTag && !isWordHtml && hasFullBleedCanvas;
|
||||
const dropGutter =
|
||||
messageSpacing === 'edge' || (messageSpacing === 'auto' && autoDropsGutter);
|
||||
const bodyPadding = dropGutter ? '0' : '1rem 1.25rem';
|
||||
const mobileBodyPaddingX = dropGutter ? '0' : '0.75rem';
|
||||
|
||||
// Word emails rely on empty <p class=MsoNormal> </p> spacers for vertical
|
||||
// rhythm. With our default line-height: 1.6 these stack into oversized gaps;
|
||||
@@ -2179,7 +2192,7 @@ export function EmailViewer({
|
||||
${wordHtmlCSS}
|
||||
${darkModeCSS}
|
||||
</style></head><body>${effectiveEmailContent.html}<style>html,body{height:auto!important;min-height:0!important;max-height:none!important}</style></body></html>`;
|
||||
}, [effectiveEmailContent.html, effectiveEmailContent.isHtml, effectiveEmailContent.hasStyleTag, effectiveEmailContent.externalBlocked, isDark, emailHasNativeDarkMode]);
|
||||
}, [effectiveEmailContent.html, effectiveEmailContent.isHtml, effectiveEmailContent.hasStyleTag, effectiveEmailContent.externalBlocked, isDark, emailHasNativeDarkMode, messageSpacing]);
|
||||
|
||||
// Unblocking external content is handled by rebuilding the iframe srcDoc:
|
||||
// toggling allowExternalContent (both "Load images" and "Trust sender" set
|
||||
|
||||
@@ -35,6 +35,7 @@ export function ReadingSettings() {
|
||||
hoverActionsCorner,
|
||||
hideInlineImageAttachments,
|
||||
attachmentImagePreviewsEnabled,
|
||||
messageSpacing,
|
||||
updateSetting,
|
||||
} = useSettingsStore();
|
||||
|
||||
@@ -115,6 +116,20 @@ export function ReadingSettings() {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{!isSettingHidden('messageSpacing') && (
|
||||
<SettingItem label={t('message_spacing.label')} description={t('message_spacing.description')} locked={isSettingLocked('messageSpacing')}>
|
||||
<Select
|
||||
value={messageSpacing}
|
||||
onChange={(value) => updateSetting('messageSpacing', value as typeof messageSpacing)}
|
||||
options={[
|
||||
{ value: 'auto', label: t('message_spacing.auto') },
|
||||
{ value: 'always', label: t('message_spacing.always') },
|
||||
{ value: 'edge', label: t('message_spacing.edge') },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{!isSettingHidden('deleteAction') && (
|
||||
<SettingItem label={t('delete_action.label')} description={t('delete_action.description')} locked={isSettingLocked('deleteAction')}>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -1121,6 +1121,13 @@
|
||||
"permanent": "Delete Permanently",
|
||||
"warning": "Emails will be permanently deleted and cannot be recovered. This action is irreversible."
|
||||
},
|
||||
"message_spacing": {
|
||||
"label": "Message Spacing",
|
||||
"description": "Padding around the message body in the reader",
|
||||
"auto": "Auto (recommended)",
|
||||
"always": "Always add spacing",
|
||||
"edge": "Edge to edge"
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "Archive in",
|
||||
"description": "How to organize emails when archiving",
|
||||
|
||||
@@ -1070,6 +1070,13 @@
|
||||
"warning": "הודעות אימייל יימחקו לצמיתות ולא ניתן לשחזר אותן. פעולה זו היא בלתי הפיכה.",
|
||||
"trash_and_read": "העבר לאשפה וסמן כקרא"
|
||||
},
|
||||
"message_spacing": {
|
||||
"label": "מרווח סביב ההודעה",
|
||||
"description": "ריווח מסביב לגוף ההודעה בתצוגת הקריאה",
|
||||
"auto": "אוטומטי (מומלץ)",
|
||||
"always": "תמיד להוסיף מרווח",
|
||||
"edge": "מקצה לקצה"
|
||||
},
|
||||
"archive_mode": {
|
||||
"label": "ארכיון ב",
|
||||
"description": "כיצד לארגן מיילים בעת אחסון בארכיון",
|
||||
|
||||
@@ -55,6 +55,14 @@ export type AttachmentPosition = 'beside-sender' | 'below-header';
|
||||
export type ToolbarPosition = 'top' | 'below-subject';
|
||||
export type ArchiveMode = 'single' | 'year' | 'month';
|
||||
export type MailLayout = 'split' | 'focus' | 'horizontal';
|
||||
|
||||
/**
|
||||
* Spacing around a message body in the reader.
|
||||
* - 'auto' : add a gutter unless the email paints its own full-bleed background
|
||||
* - 'always': always add the gutter
|
||||
* - 'edge' : never add one (render edge-to-edge)
|
||||
*/
|
||||
export type MessageSpacing = 'auto' | 'always' | 'edge';
|
||||
export type CalendarHoverPreview = 'off' | 'instant' | 'delay-500ms' | 'delay-1s' | 'delay-2s';
|
||||
export type SendDelaySeconds = 0 | 10 | 30 | 60;
|
||||
export type ProtocolOpenMode = 'active-session' | 'new-tab';
|
||||
@@ -156,6 +164,7 @@ interface SettingsState {
|
||||
mailLayout: MailLayout;
|
||||
emailsPerPage: number;
|
||||
externalContentPolicy: ExternalContentPolicy;
|
||||
messageSpacing: MessageSpacing; // Gutter around the message body in the reader
|
||||
mailAttachmentAction: MailAttachmentAction;
|
||||
attachmentPosition: AttachmentPosition;
|
||||
emailAlwaysLightMode: boolean; // Always render email content in light mode
|
||||
@@ -378,6 +387,7 @@ const DEFAULT_SETTINGS = {
|
||||
mailLayout: 'split' as MailLayout,
|
||||
emailsPerPage: 50,
|
||||
externalContentPolicy: 'ask' as ExternalContentPolicy,
|
||||
messageSpacing: 'auto' as MessageSpacing,
|
||||
mailAttachmentAction: 'preview' as MailAttachmentAction,
|
||||
attachmentPosition: 'beside-sender' as AttachmentPosition,
|
||||
emailAlwaysLightMode: false,
|
||||
@@ -591,6 +601,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
mailLayout: state.mailLayout,
|
||||
emailsPerPage: state.emailsPerPage,
|
||||
externalContentPolicy: state.externalContentPolicy,
|
||||
messageSpacing: state.messageSpacing,
|
||||
mailAttachmentAction: state.mailAttachmentAction,
|
||||
attachmentPosition: state.attachmentPosition,
|
||||
archiveMode: state.archiveMode,
|
||||
|
||||
Reference in New Issue
Block a user