Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion components/email/email-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2194,9 +2194,10 @@ export function EmailViewer({
word's min-content width so columns are not collapsed to a single char. */
td, th { overflow-wrap: break-word; }
pre { white-space: pre-wrap; word-wrap: break-word; }
[dir="auto"] { unicode-bidi: plaintext; }
${wordHtmlCSS}
${darkModeCSS}
</style></head><body>${effectiveEmailContent.html}<style>html,body{height:auto!important;min-height:0!important;max-height:none!important}</style></body></html>`;
</style></head><body${effectiveEmailContent.isHtml ? '' : ' dir="auto"'}>${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, messageSpacing]);

// Unblocking external content is handled by rebuilding the iframe srcDoc:
Expand Down
27 changes: 20 additions & 7 deletions components/email/rich-text-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import Heading from "@tiptap/extension-heading";
import Underline from "@tiptap/extension-underline";
import Link from "@tiptap/extension-link";
import TextAlign from "@tiptap/extension-text-align";
import { TextDirection } from "@/components/email/text-direction";
// NOTE: @tiptap/core v3 ships a built-in TextDirection extension (auto-loaded
// via enableCoreExtensions). We no longer ship a custom one; configure it via
// the `textDirection` editor option below.
import { TextStyle } from "@tiptap/extension-text-style";
import Color from "@tiptap/extension-color";
import { ResizableImage } from "@/components/email/resizable-image";
Expand All @@ -32,7 +34,8 @@ import {
AlignLeft,
AlignCenter,
AlignRight,
ArrowLeftRight,
ArrowRightToLine as LtrIcon,
ArrowLeftToLine as RtlIcon,
Link as LinkIcon,
Undo,
Redo,
Expand Down Expand Up @@ -255,8 +258,12 @@ export function RichTextEditor({
// rich/branded signatures keep their inline styling in the editor and
// in the sent mail (see signature-block.ts).
SignatureBlock,
TextDirection,
],
// Enable @tiptap/core v3's built-in TextDirection extension with "auto"
// default. Each block auto-detects direction from its first strong
// character (RTL for Farsi/Arabic/Hebrew, LTR for Latin). The toolbar
// button can still pin an explicit ltr/rtl per block.
textDirection: "auto",
content,
editorProps: {
attributes: {
Expand Down Expand Up @@ -530,16 +537,22 @@ export function RichTextEditor({
{rtlEditingSupport && (
<ToolbarButton
active={
(editor.getAttributes("paragraph").dir || editor.getAttributes("heading").dir) === "rtl"
(editor.getAttributes("paragraph").dir || editor.getAttributes("heading").dir || "auto")
=== "rtl"
}
onClick={() => {
const cur =
editor.getAttributes("paragraph").dir || editor.getAttributes("heading").dir;
editor.chain().focus().setTextDirection(cur === "rtl" ? "ltr" : "rtl").run();
editor.getAttributes("paragraph").dir ||
editor.getAttributes("heading").dir ||
"auto";
const next = cur === "rtl" ? "ltr" : "rtl";
editor.chain().focus().setTextDirection(next).run();
}}
title={tToolbar("text_direction")}
>
<ArrowLeftRight className="w-4 h-4" />
{(editor.getAttributes("paragraph").dir || editor.getAttributes("heading").dir) === "rtl"
? <RtlIcon className="w-4 h-4" />
: <LtrIcon className="w-4 h-4" />}
</ToolbarButton>
)}

Expand Down
63 changes: 0 additions & 63 deletions components/email/text-direction.ts

This file was deleted.

2 changes: 1 addition & 1 deletion lib/email-sanitization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const EMAIL_SANITIZE_CONFIG = {
// components/email/quoted-html.ts). Explicitly whitelisted despite
// ALLOW_DATA_ATTR:false so the viewer can detect and collapse the quoted
// original (lib/quote-collapse.ts); it's inert otherwise.
ADD_ATTR: ['target', 'rel', 'style', 'class', 'width', 'height', 'align', 'valign', 'bgcolor', 'color', 'data-quoted-html'],
ADD_ATTR: ['target', 'rel', 'style', 'class', 'width', 'height', 'align', 'valign', 'bgcolor', 'color', 'dir', 'data-quoted-html'],
ALLOW_DATA_ATTR: false,
FORCE_BODY: true,
// Allow blob: URIs so authenticated inline images (CID) are not stripped.
Expand Down
118 changes: 59 additions & 59 deletions stores/settings-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ const DEFAULT_SETTINGS = {
defaultReplyMode: 'reply' as ReplyMode,
autoSelectReplyIdentity: false,
plainTextMode: false,
rtlEditingSupport: false,
rtlEditingSupport: true,
subAddressDelimiter: DEFAULT_SUB_ADDRESS_DELIMITER,
sendDelaySeconds: 0 as SendDelaySeconds,
signaturePosition: 'below_quote' as SignaturePosition,
Expand Down Expand Up @@ -1023,64 +1023,64 @@ export const useSettingsStore = create<SettingsState>()(
*/
export function migrateSettings(persisted: unknown, version: number): SettingsState {
const state = persisted as Record<string, unknown>;
if (version < 2 && state.listDensity) {
state.density = state.listDensity;
delete state.listDensity;
}
if (![0, 10, 30, 60].includes(state.sendDelaySeconds as number)) {
state.sendDelaySeconds = 0;
}
if (version < 3 && typeof state.protocolOpenMode !== 'string' && typeof state.protocolMailtoOpenMode === 'string') {
state.protocolOpenMode = state.protocolMailtoOpenMode;
}
delete state.protocolMailtoOpenMode;
// v4: `dateFormat` was repurposed from 'regional'|'iso'|'custom' to
// 'smart'|'relative'|'full'. The old setting was never read anywhere,
// so every persisted value maps to the new default.
if (version < 4) {
state.dateFormat = 'smart';
}
// v5: allMailFolderIds went from a global `string[] | null` to a
// per-account `Record<accountId, string[]>`. The legacy global list
// can't be attributed to a specific account here (the active account
// isn't known at migrate time), so it's dropped - each account starts
// "not configured" (defaults to all no-role folders).
if (version < 5 || !isPlainRecord(state.allMailFolderIds)) {
state.allMailFolderIds = {};
}
// "All accounts" was reworked into the account-bounded "Unified
// Mailbox". The standalone __all_mail__ view (`enableAllMailView`) was
// folded into the unified "All mail" entry (`enableCrossAllView`), and a
// `unifiedCrossAccount` toggle now governs whether the views span every
// logged-in account. Existing users keep their current behaviour:
// - if any cross view was on, they were already cross-account -> keep it on
// - else if only standalone All Mail was on, enable the account-bounded
// unified "All mail" entry (folder selection carries over via allMailFolderIds)
// Guarded at <7 (not <6) so users who stopped at main's interim v6
// identity-map bump - which shipped without this rework - still receive it.
if (version < 7) {
const hadCross = !!(state.enableCrossUnreadView || state.enableCrossStarredView || state.enableCrossAllView);
if (hadCross) {
state.unifiedCrossAccount = true;
} else if (state.enableAllMailView) {
state.enableUnifiedMailbox = true;
state.enableCrossAllView = true;
state.unifiedCrossAccount = false;
}
delete state.enableAllMailView;
if (typeof state.unifiedCrossAccount !== 'boolean') state.unifiedCrossAccount = false;
// The reworked unified mailbox spans the account's own folders plus its
// shared/group folders, so enable shared inclusion for every migrated
// configuration (matches the new-install default).
state.includeGroupInUnified = true;
}
// Per-account default-identity map (issue #507). Coerce any
// missing/legacy value to an empty record. Guarded at <6 so users who
// already received it via main's v6 bump keep their populated map.
if (version < 6 || !isPlainRecord(state.preferredIdentityIds)) {
state.preferredIdentityIds = {};
}
return state as unknown as SettingsState;
if (version < 2 && state.listDensity) {
state.density = state.listDensity;
delete state.listDensity;
}
if (![0, 10, 30, 60].includes(state.sendDelaySeconds as number)) {
state.sendDelaySeconds = 0;
}
if (version < 3 && typeof state.protocolOpenMode !== 'string' && typeof state.protocolMailtoOpenMode === 'string') {
state.protocolOpenMode = state.protocolMailtoOpenMode;
}
delete state.protocolMailtoOpenMode;
// v4: `dateFormat` was repurposed from 'regional'|'iso'|'custom' to
// 'smart'|'relative'|'full'. The old setting was never read anywhere,
// so every persisted value maps to the new default.
if (version < 4) {
state.dateFormat = 'smart';
}
// v5: allMailFolderIds went from a global `string[] | null` to a
// per-account `Record<accountId, string[]>`. The legacy global list
// can't be attributed to a specific account here (the active account
// isn't known at migrate time), so it's dropped - each account starts
// "not configured" (defaults to all no-role folders).
if (version < 5 || !isPlainRecord(state.allMailFolderIds)) {
state.allMailFolderIds = {};
}
// "All accounts" was reworked into the account-bounded "Unified
// Mailbox". The standalone __all_mail__ view (`enableAllMailView`) was
// folded into the unified "All mail" entry (`enableCrossAllView`), and a
// `unifiedCrossAccount` toggle now governs whether the views span every
// logged-in account. Existing users keep their current behaviour:
// - if any cross view was on, they were already cross-account -> keep it on
// - else if only standalone All Mail was on, enable the account-bounded
// unified "All mail" entry (folder selection carries over via allMailFolderIds)
// Guarded at <7 (not <6) so users who stopped at main's interim v6
// identity-map bump - which shipped without this rework - still receive it.
if (version < 7) {
const hadCross = !!(state.enableCrossUnreadView || state.enableCrossStarredView || state.enableCrossAllView);
if (hadCross) {
state.unifiedCrossAccount = true;
} else if (state.enableAllMailView) {
state.enableUnifiedMailbox = true;
state.enableCrossAllView = true;
state.unifiedCrossAccount = false;
}
delete state.enableAllMailView;
if (typeof state.unifiedCrossAccount !== 'boolean') state.unifiedCrossAccount = false;
// The reworked unified mailbox spans the account's own folders plus its
// shared/group folders, so enable shared inclusion for every migrated
// configuration (matches the new-install default).
state.includeGroupInUnified = true;
}
// Per-account default-identity map (issue #507). Coerce any
// missing/legacy value to an empty record. Guarded at <6 so users who
// already received it via main's v6 bump keep their populated map.
if (version < 6 || !isPlainRecord(state.preferredIdentityIds)) {
state.preferredIdentityIds = {};
}
return state as unknown as SettingsState;
}

// Helper functions to apply settings to DOM
Expand Down