fix: let senders, subjects and previews in the list and reader take their own text direction, so English text in Arabic is cut at its end

This commit is contained in:
Linus Rath
2026-10-28 11:06:18 +01:00
parent 7e25b9d7fa
commit 0532fb2a47
4 changed files with 41 additions and 19 deletions
@@ -875,3 +875,25 @@ describe('ThreadListItem swipe on a conversation', () => {
expect(onArchive).toHaveBeenCalledWith(expect.objectContaining({ id: 'b' }));
});
});
// R11: in an Arabic interface an English subject was cut on the wrong side.
// The text takes its direction from itself, wherever it sits.
describe('ThreadListItem text direction', () => {
beforeEach(() => {
useSettingsStore.setState({ emailKeywords: [...DEFAULT_KEYWORDS], showPreview: true, mailLayout: 'split', interfaceLayout: 'default' });
});
it.each([
['a single message', () => groupEmailsByThread([makeEmail({ preview: 'Preview text here' })])[0]],
['a conversation', () => groupEmailsByThread([
makeEmail({ id: 'a', receivedAt: '2024-01-15T10:00:00Z', preview: 'Preview text here' }),
makeEmail({ id: 'b', receivedAt: '2024-01-15T11:00:00Z', preview: 'Preview text here' }),
])[0]],
])('gives the sender, subject and preview of %s dir="auto"', (_name, makeThread) => {
render(<ThreadListItem thread={makeThread()} isExpanded={false} onToggleExpand={() => {}} onEmailSelect={() => {}} />);
for (const text of ['Alice', 'Test Subject', 'Preview text here']) {
const el = screen.getAllByText((_c, node) => node?.textContent?.trim() === text && node.children.length === 0)[0];
expect(el.closest('[dir="auto"]'), text).not.toBeNull();
}
});
});
+2 -2
View File
@@ -760,7 +760,7 @@ function MvConversationCard({ email, onOpen, timeFormat, compact }: { email: Ema
<Avatar name={sender?.name} email={sender?.email} size="md" className="w-10 h-10 flex-shrink-0" />
<span className="flex-1 min-w-0">
<span className="flex items-baseline gap-2">
<span className={cn("text-sm truncate", unread ? "font-bold" : "font-semibold")}>
<span dir="auto" className={cn("text-sm truncate", unread ? "font-bold" : "font-semibold")}>
{sender?.name || sender?.email || ''}
</span>
<span className="ms-auto ps-2 text-xs text-muted-foreground whitespace-nowrap">
@@ -876,7 +876,7 @@ export function MvExpandedConversationCard({
<Avatar name={sender?.name} email={sender?.email} size="md" className="w-10 h-10 flex-shrink-0" />
<span className="flex-1 min-w-0">
<span className="flex items-baseline gap-2">
<span className="text-sm font-semibold truncate">{sender?.name || sender?.email || ''}</span>
<span dir="auto" className="text-sm font-semibold truncate">{sender?.name || sender?.email || ''}</span>
{!compact && sender?.name && sender.email && (
<span className="text-xs text-muted-foreground truncate">&lt;{sender.email}&gt;</span>
)}
@@ -389,7 +389,7 @@ function EmailCard({
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className={cn(
<span dir="auto" className={cn(
"font-medium truncate",
isUnread ? "text-foreground" : "text-muted-foreground"
)}>
@@ -406,7 +406,7 @@ function EmailCard({
{formatDate(emailDisplayDate(email))}
</div>
{!isExpanded && density !== 'extra-compact' && (
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
<p dir="auto" className="text-sm text-muted-foreground mt-1 line-clamp-2">
{cleanPreview(email.preview) || t('email_viewer.no_preview_available')}
</p>
)}
+15 -15
View File
@@ -675,7 +675,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
<span className="sr-only">{accountDescription}</span>
</span>
)}
<span className={cn(
<span dir={columns ? undefined : "auto"} className={cn(
columns ? cn(LIST_COLUMN_CLASS.from, 'flex items-center gap-2') : 'w-32 shrink-0 truncate lg:w-40',
'text-sm',
isUnread ? (mvRow ? 'font-bold text-foreground' : 'font-semibold text-foreground') : 'font-medium text-foreground/80'
@@ -693,7 +693,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
<span className="sr-only">{accountDescription}</span>
</span>
)}
<span className="min-w-0 truncate">
<span dir="auto" className="min-w-0 truncate">
{sender?.name || (sender?.email && formatAddress(sender.email)) || tUnknown}
</span>
</>
@@ -707,7 +707,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
))}
</span>
)}
<span className={cn(
<span dir="auto" className={cn(
'min-w-0 truncate',
isUnread ? (mvRow ? 'font-bold text-foreground' : 'font-semibold text-foreground') : 'text-foreground/90'
)}>
@@ -715,7 +715,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
</span>
<PluginRowBadges email={email} className={mvRow ? "ms-2" : undefined} />
{(previewSnippet || inlinePreview) && (
<span className="min-w-0 shrink-[9999] truncate text-muted-foreground">
<span dir="auto" className="min-w-0 shrink-[9999] truncate text-muted-foreground">
{/* Gmail runs the snippet on from the subject after a dash. */}
{mvRow && '\u00a0-\u00a0'}
{previewSnippet ? <> <SearchSnippetText snippet={previewSnippet} /></> : (mvRow ? trimmedPreview : inlinePreview)}
@@ -773,7 +773,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
<span className="sr-only">{accountDescription}</span>
</span>
)}
<span className={cn(
<span dir="auto" className={cn(
"truncate text-sm",
isUnread
? "font-bold text-foreground"
@@ -850,7 +850,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
))}
</span>
)}
<span className={cn(
<span dir="auto" className={cn(
"min-w-0 flex-1 truncate text-sm",
isUnread
? "font-semibold text-foreground"
@@ -863,7 +863,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
{showPreview && density !== 'extra-compact' && density !== 'compact' && (
<div className={cn("flex min-w-0 items-center gap-1", !mvMobileRow && "contents")}>
<p className={cn(
<p dir="auto" className={cn(
"text-sm leading-relaxed",
mvMobileRow ? "line-clamp-1 min-w-0 flex-1" : "line-clamp-2",
isUnread
@@ -1379,7 +1379,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
<span className="sr-only">{threadAccountDescription}</span>
</span>
)}
<span className={cn('min-w-0 truncate', hasUnread ? 'font-semibold text-foreground' : 'font-medium text-foreground/80')}>
<span dir="auto" className={cn('min-w-0 truncate', hasUnread ? 'font-semibold text-foreground' : 'font-medium text-foreground/80')}>
{displayNames.join(', ')}
</span>
<span className="shrink-0 text-xs text-muted-foreground" title={t('messages_tooltip', { count: emailCount })}>
@@ -1389,7 +1389,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
) : mvRow ? (
// Gmail puts the message count after the names, in grey.
<span data-mv-names="" className="flex w-32 shrink-0 items-baseline gap-1 text-sm lg:w-40">
<span className={cn('min-w-0 truncate', hasUnread ? 'font-bold text-foreground' : 'font-medium text-foreground/80')}>
<span dir="auto" className={cn('min-w-0 truncate', hasUnread ? 'font-bold text-foreground' : 'font-medium text-foreground/80')}>
{displayNames.join(', ')}
</span>
<span className="shrink-0 text-xs text-muted-foreground" title={t('messages_tooltip', { count: emailCount })}>
@@ -1397,7 +1397,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
</span>
</span>
) : (
<span className={cn(
<span dir="auto" className={cn(
// Matches SingleEmailItem: the sender column sets where
// every row's tags and subject begin, so the two have to
// agree or thread rows sit 1rem further right.
@@ -1422,7 +1422,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
))}
</span>
)}
<span className={cn(
<span dir="auto" className={cn(
'min-w-0 truncate',
hasUnread ? (mvRow ? 'font-bold text-foreground' : 'font-semibold text-foreground') : 'text-foreground/90'
)}>
@@ -1430,7 +1430,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
</span>
<PluginRowBadges email={latestEmail} className={mvRow ? "ms-2" : undefined} />
{(previewSnippet || inlinePreview) && (
<span className="min-w-0 shrink-[9999] truncate text-muted-foreground">
<span dir="auto" className="min-w-0 shrink-[9999] truncate text-muted-foreground">
{mvRow && '\u00a0-\u00a0'}
{previewSnippet ? <> <SearchSnippetText snippet={previewSnippet} /></> : (mvRow ? trimmedPreview : inlinePreview)}
</span>
@@ -1492,7 +1492,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
<span className="sr-only">{threadAccountDescription}</span>
</span>
)}
<span className={cn(
<span dir="auto" className={cn(
"truncate text-sm",
hasUnread
? "font-bold text-foreground"
@@ -1572,7 +1572,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
))}
</span>
)}
<span className={cn(
<span dir="auto" className={cn(
"min-w-0 flex-1 truncate text-sm",
hasUnread
? "font-semibold text-foreground"
@@ -1585,7 +1585,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
{showPreview && density !== 'extra-compact' && density !== 'compact' && (
<div className={cn("flex min-w-0 items-center gap-1", !mvMobileRow && "contents")}>
<p className={cn(
<p dir="auto" className={cn(
"text-sm leading-relaxed",
mvMobileRow ? "line-clamp-1 min-w-0 flex-1" : "line-clamp-2",
hasUnread