mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 16:07:12 +00:00
fix: show own IDN domains as written in the mail list rows (bulwarkmail/webmail#1100)
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { listAddressLabel } from '../list-sender';
|
||||
import { toUnicodeEmailOnOwnDomains } from '../idn';
|
||||
|
||||
// A sender with no display name on one of the user's own IDN domains showed
|
||||
// as punycode in the list rows (`info@xn--bcher-kva.de`), while the message
|
||||
// header already read `info@bücher.de` (webmail thread-list-item.tsx, #1100).
|
||||
|
||||
const own = ['me@xn--bcher-kva.de'];
|
||||
const format = (address: string) => toUnicodeEmailOnOwnDomains(address, own);
|
||||
|
||||
describe('listAddressLabel', () => {
|
||||
it('reads a bare address on an own IDN domain as written, and leaves others alone', () => {
|
||||
expect(listAddressLabel({ email: 'info@xn--bcher-kva.de' }, format, 'Unknown')).toBe('info@bücher.de');
|
||||
expect(listAddressLabel({ email: 'info@xn--mnchen-3ya.de' }, format, 'Unknown')).toBe('info@xn--mnchen-3ya.de');
|
||||
});
|
||||
|
||||
it('prefers the display name and falls back to the unknown label', () => {
|
||||
expect(listAddressLabel({ name: 'Bücher', email: 'info@xn--bcher-kva.de' }, format, 'Unknown')).toBe('Bücher');
|
||||
expect(listAddressLabel(undefined, format, 'Unknown')).toBe('Unknown');
|
||||
expect(listAddressLabel({ name: '', email: '' }, format, 'Unknown')).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('list rows', () => {
|
||||
const read = (file: string) => fs.readFileSync(path.resolve(__dirname, '../../screens', file), 'utf8');
|
||||
|
||||
it('name senders and recipients through the own-domain formatter', () => {
|
||||
const list = read('EmailListScreen.tsx');
|
||||
// Read once by the screen and handed to the memoised rows.
|
||||
expect(list).toMatch(/const formatAddress = useOwnDomainAddress\(\);/);
|
||||
expect(list).toMatch(/formatAddress=\{formatAddress\}/);
|
||||
expect(list.match(/listAddressLabel\([^)]*formatAddress/g)).toHaveLength(2);
|
||||
expect(list).not.toMatch(/\.name \|\| [\w.?[\]]*\.email/);
|
||||
|
||||
const unified = read('UnifiedInboxScreen.tsx');
|
||||
expect(unified).toMatch(/const formatAddress = useOwnDomainAddress\(\);/);
|
||||
expect(unified.match(/listAddressLabel\([^)]*formatAddress/g)).toHaveLength(2);
|
||||
expect(unified.match(/senderName\(item, [^)]*\), formatAddress\)/g)).toHaveLength(2);
|
||||
expect(unified).not.toMatch(/\.name \|\| [\w.?[\]]*\.email/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* How a mail list row names an address: its display name, else the address
|
||||
* itself, formatted by the caller (useOwnDomainAddress: the user's own IDN
|
||||
* domains read as written, `bücher.de`), else the "unknown" label (webmail
|
||||
* components/email/thread-list-item.tsx, #1100).
|
||||
*/
|
||||
export function listAddressLabel(
|
||||
address: { name?: string | null; email?: string | null } | undefined,
|
||||
formatAddress: (address: string) => string,
|
||||
unknown: string,
|
||||
): string {
|
||||
return address?.name || (address?.email ? formatAddress(address.email) : '') || unknown;
|
||||
}
|
||||
@@ -50,6 +50,8 @@ import { useContactsStore } from '../stores/contacts-store';
|
||||
import { useOutboxStore } from '../stores/outbox-store';
|
||||
import { toast } from '../stores/toast-store';
|
||||
import { getContactDisplayName } from '../lib/contact-utils';
|
||||
import { listAddressLabel } from '../lib/list-sender';
|
||||
import { useOwnDomainAddress } from '../lib/use-own-domain-address';
|
||||
import { formatListDate } from '../lib/date-format';
|
||||
import { singleLine } from '../lib/single-line';
|
||||
import { cleanPreview } from '../lib/preview-text';
|
||||
@@ -85,21 +87,20 @@ function reportKeywordFailure(title: string, err: unknown): void {
|
||||
toast.error(title, err instanceof Error ? err.message : undefined);
|
||||
}
|
||||
|
||||
function getSenderName(email: Email, unknownLabel: string): string {
|
||||
return email.from?.[0]?.name || email.from?.[0]?.email || unknownLabel;
|
||||
}
|
||||
|
||||
function getSenderEmail(email: Email): string | undefined {
|
||||
return email.from?.[0]?.email;
|
||||
}
|
||||
|
||||
// Sent/Drafts rows name the recipient, not "me" (webmail 1.4.12).
|
||||
function getCounterpart(email: Email, showRecipient: boolean, unknownLabel: string): { name: string; email?: string } {
|
||||
// Sent/Drafts rows name the recipient, not "me" (webmail 1.4.12). A bare
|
||||
// address on one of the user's own IDN domains reads as written (#1100).
|
||||
function getCounterpart(
|
||||
email: Email,
|
||||
showRecipient: boolean,
|
||||
unknownLabel: string,
|
||||
formatAddress: (address: string) => string,
|
||||
): { name: string; email?: string } {
|
||||
if (showRecipient) {
|
||||
const to = email.to?.[0] ?? email.cc?.[0] ?? email.bcc?.[0];
|
||||
if (to) return { name: to.name || to.email, email: to.email };
|
||||
if (to) return { name: listAddressLabel(to, formatAddress, unknownLabel), email: to.email };
|
||||
}
|
||||
return { name: getSenderName(email, unknownLabel), email: getSenderEmail(email) };
|
||||
const from = email.from?.[0];
|
||||
return { name: listAddressLabel(from, formatAddress, unknownLabel), email: from?.email };
|
||||
}
|
||||
|
||||
function isUnread(email: Email): boolean {
|
||||
@@ -136,6 +137,7 @@ const EmailRow = React.memo(function EmailRow({
|
||||
threadCount,
|
||||
showPreview,
|
||||
showRecipient,
|
||||
formatAddress,
|
||||
tagIds,
|
||||
keywordDefs,
|
||||
disableAvatarImages,
|
||||
@@ -153,6 +155,8 @@ const EmailRow = React.memo(function EmailRow({
|
||||
threadCount: number;
|
||||
showPreview: boolean;
|
||||
showRecipient: boolean;
|
||||
/** useOwnDomainAddress, read once by the list rather than per row. */
|
||||
formatAddress: (address: string) => string;
|
||||
/** Comma-joined tag ids of the row (thread union) — a string so memo holds. */
|
||||
tagIds: string;
|
||||
keywordDefs: KeywordDef[];
|
||||
@@ -181,6 +185,7 @@ const EmailRow = React.memo(function EmailRow({
|
||||
item,
|
||||
showRecipient,
|
||||
tr('email_viewer.unknown_sender', 'Unknown'),
|
||||
formatAddress,
|
||||
);
|
||||
const unread = isUnread(item);
|
||||
const starred = isStarred(item);
|
||||
@@ -570,6 +575,7 @@ export default function EmailListScreen({
|
||||
const folderKeywordDefs = (currentOwnerAccountId && keywordDefsByAccount.get(currentOwnerAccountId)) || keywordDefs;
|
||||
const inJunk = currentRole === 'junk' || currentRole === 'spam';
|
||||
const showRecipient = currentRole === 'sent' || currentRole === 'drafts';
|
||||
const formatAddress = useOwnDomainAddress();
|
||||
|
||||
// When threading is on, collapse same-thread emails so the list shows the
|
||||
// newest message per thread with a count badge (pinned threads first).
|
||||
@@ -885,6 +891,7 @@ export default function EmailListScreen({
|
||||
threadCount={threadCountFor(item)}
|
||||
showPreview={showPreview}
|
||||
showRecipient={showRecipient}
|
||||
formatAddress={formatAddress}
|
||||
tagIds={rowTagIds.get(key) ?? ''}
|
||||
keywordDefs={(item.jmapAccountId && keywordDefsByAccount.get(item.jmapAccountId)) || folderKeywordDefs}
|
||||
disableAvatarImages={inJunk && !showAvatarsInJunk}
|
||||
@@ -903,7 +910,7 @@ export default function EmailListScreen({
|
||||
[
|
||||
selectedIds, activeRowKey, selectionMode, handleRowPress, toggleSelect, swipeLeftAction, swipeRightAction,
|
||||
swipeMode, handleRowSwipe, disableThreading, rowFlags, rowTagIds, threadCountFor,
|
||||
showPreview, showRecipient, keywordDefsByAccount, folderKeywordDefs, inJunk, showAvatarsInJunk,
|
||||
showPreview, showRecipient, formatAddress, keywordDefsByAccount, folderKeywordDefs, inJunk, showAvatarsInJunk,
|
||||
loadAttachments, openAttachment,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -25,6 +25,8 @@ import { useSettingsStore, type SwipeAction } from '../stores/settings-store';
|
||||
import { useLocaleStore } from '../stores/locale-store';
|
||||
import { formatListDate } from '../lib/date-format';
|
||||
import { singleLine } from '../lib/single-line';
|
||||
import { listAddressLabel } from '../lib/list-sender';
|
||||
import { useOwnDomainAddress } from '../lib/use-own-domain-address';
|
||||
import { cleanPreview } from '../lib/preview-text';
|
||||
import { prefetchMessage, rememberRows } from '../lib/email-detail-cache';
|
||||
import { isPermanentDelete, confirmPermanentDelete } from '../lib/delete-confirm';
|
||||
@@ -39,12 +41,18 @@ type Props = NativeStackScreenProps<RootStackParamList, 'UnifiedInbox'>;
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
function senderName(email: UnifiedEmail, showRecipient: boolean, unknown: string): string {
|
||||
// A bare address on one of the user's own IDN domains reads as written (#1100).
|
||||
function senderName(
|
||||
email: UnifiedEmail,
|
||||
showRecipient: boolean,
|
||||
unknown: string,
|
||||
formatAddress: (address: string) => string,
|
||||
): string {
|
||||
if (showRecipient) {
|
||||
const to = email.to?.[0];
|
||||
if (to) return to.name || to.email;
|
||||
if (to) return listAddressLabel(to, formatAddress, unknown);
|
||||
}
|
||||
return email.from?.[0]?.name || email.from?.[0]?.email || unknown;
|
||||
return listAddressLabel(email.from?.[0], formatAddress, unknown);
|
||||
}
|
||||
|
||||
function rowKey(e: UnifiedEmail): string {
|
||||
@@ -326,6 +334,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
||||
|
||||
const errorCount = Object.keys(errors).length;
|
||||
const showRecipient = role === 'sent' || role === 'drafts';
|
||||
const formatAddress = useOwnDomainAddress();
|
||||
const title = view
|
||||
? t(`sidebar.unified_all_${view === 'all' ? 'mail' : view}`, { all: 'All mail', unread: 'All unread', starred: 'All starred' }[view])
|
||||
: role === 'inbox'
|
||||
@@ -366,7 +375,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
||||
)}
|
||||
{density.showAvatar && <View style={styles.avatarWrap}>
|
||||
<SenderAvatar
|
||||
name={senderName(item, showRecipient, t('email_viewer.unknown_sender', 'Unknown'))}
|
||||
name={senderName(item, showRecipient, t('email_viewer.unknown_sender', 'Unknown'), formatAddress)}
|
||||
email={showRecipient ? item.to?.[0]?.email : item.from?.[0]?.email}
|
||||
size={componentSizes.avatarMd}
|
||||
disableImages={role === 'junk' && !showAvatarsInJunk}
|
||||
@@ -379,7 +388,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
||||
<View style={styles.content}>
|
||||
<View style={styles.line}>
|
||||
<Text style={[styles.sender, dyn.bodyMedium, unread && styles.bold]} numberOfLines={1}>
|
||||
{senderName(item, showRecipient, t('email_viewer.unknown_sender', 'Unknown'))}
|
||||
{senderName(item, showRecipient, t('email_viewer.unknown_sender', 'Unknown'), formatAddress)}
|
||||
</Text>
|
||||
{starred && <Star size={12} color={c.starred} fill={c.starred} />}
|
||||
{item.hasAttachment && <Paperclip size={12} color={c.textMuted} />}
|
||||
|
||||
Reference in New Issue
Block a user