fix: show the sender on search hits from other folders when searching from Sent

This commit is contained in:
Linus Rath
2026-10-31 13:52:38 +01:00
parent ccf93ed667
commit ff878fc6bf
3 changed files with 72 additions and 6 deletions
+38 -1
View File
@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { listAddressLabel } from '../list-sender';
import { listAddressLabel, rowShowsRecipient } from '../list-sender';
import { toUnicodeEmailOnOwnDomains } from '../idn';
// A sender with no display name on one of the user's own IDN domains showed
@@ -42,3 +42,40 @@ describe('list rows', () => {
expect(unified).not.toMatch(/\.name \|\| [\w.?[\]]*\.email/);
});
});
// A search started in Sent spans every folder: Inbox hits showed their
// recipient (the user) instead of their sender.
describe('rowShowsRecipient', () => {
const OWN = [
{ id: 'inb', role: 'inbox' },
{ id: 'snt', role: 'sent' },
{ id: 'drf', role: 'drafts' },
];
const SHARED = [
{ id: 'grp-1:inb', originalId: 'inb', role: 'inbox' },
{ id: 'grp-1:snt', originalId: 'snt', role: 'sent' },
];
it('follows the open folder for its own list', () => {
expect(rowShowsRecipient({ inb: true }, [], 'sent', 'current')).toBe(true);
expect(rowShowsRecipient({ snt: true }, [], 'inbox', 'current')).toBe(false);
});
it("follows each row's own folders in a search over every folder", () => {
expect(rowShowsRecipient({ inb: true }, OWN, 'sent', 'all')).toBe(false);
expect(rowShowsRecipient({ snt: true }, OWN, 'sent', 'all')).toBe(true);
expect(rowShowsRecipient({ drf: true }, OWN, 'inbox', 'all')).toBe(true);
expect(rowShowsRecipient({ inb: true }, OWN, 'sent', 'inb')).toBe(false);
});
it("matches a shared account's folders on their raw ids", () => {
expect(rowShowsRecipient({ snt: true }, SHARED, 'sent', 'all')).toBe(true);
expect(rowShowsRecipient({ inb: true }, SHARED, 'sent', 'all')).toBe(false);
});
it('is wired into the list rows', () => {
const list = fs.readFileSync(path.resolve(__dirname, '..', '..', 'screens/EmailListScreen.tsx'), 'utf8');
expect(list).toMatch(/showRecipient=\{showRecipientFor\(item\)\}/);
expect(list).not.toMatch(/const showRecipient = currentRole === 'sent'/);
});
});
+18
View File
@@ -11,3 +11,21 @@ export function listAddressLabel(
): string {
return address?.name || (address?.email ? formatAddress(address.email) : '') || unknown;
}
/**
* Whether a list row names its recipient instead of its sender. Sent and
* Drafts name the recipient (webmail 1.4.12). A search started there runs
* over every folder unless it is narrowed (`folderScope` other than
* 'current'), so the open folder says nothing about a row: the row's own
* folders decide, matched on each folder's raw id. `rowMailboxes` are the
* folders of the row's account.
*/
export function rowShowsRecipient(
mailboxIds: Record<string, boolean> | undefined,
rowMailboxes: ReadonlyArray<{ id: string; originalId?: string; role?: string | null }>,
listRole: string | null | undefined,
folderScope: string,
): boolean {
if (folderScope === 'current') return listRole === 'sent' || listRole === 'drafts';
return rowMailboxes.some((m) => (m.role === 'sent' || m.role === 'drafts') && !!mailboxIds?.[m.originalId ?? m.id]);
}
+16 -5
View File
@@ -53,14 +53,14 @@ 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 { listAddressLabel, rowShowsRecipient } 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';
import { SearchSnippetText } from '../components/email/SearchSnippetText';
import {
findTrashMailbox, findArchiveMailbox, findJunkMailbox, mailboxesForSiblingOf, ownMailboxes,
findTrashMailbox, findArchiveMailbox, findJunkMailbox, mailboxesForSiblingOf, ownMailboxes, mailboxesOfAccount,
} from '../lib/mailbox-tree';
import { localizeMailboxName } from '../lib/mailbox-label';
import {
@@ -577,7 +577,18 @@ export default function EmailListScreen({
}, [sharedTags, keywordDefs]);
const folderKeywordDefs = (currentOwnerAccountId && keywordDefsByAccount.get(currentOwnerAccountId)) || keywordDefs;
const inJunk = currentRole === 'junk' || currentRole === 'spam';
const showRecipient = currentRole === 'sent' || currentRole === 'drafts';
// A search started in Sent runs over every folder: each row names its
// recipient only when it is in Sent or Drafts itself.
const rowScope = effectiveFolderScope(storeSearchQuery, filters, currentRole);
const showRecipientFor = React.useCallback(
(item: Email) => rowShowsRecipient(
item.mailboxIds,
rowScope === 'current' ? [] : mailboxesOfAccount(mailboxes, accountIdOfRow(item)),
currentRole,
rowScope,
),
[rowScope, mailboxes, currentRole],
);
const formatAddress = useOwnDomainAddress();
// When threading is on, collapse same-thread emails so the list shows the
@@ -896,7 +907,7 @@ export default function EmailListScreen({
item={item}
threadCount={threadCountFor(item)}
showPreview={showPreview}
showRecipient={showRecipient}
showRecipient={showRecipientFor(item)}
formatAddress={formatAddress}
tagIds={rowTagIds.get(key) ?? ''}
keywordDefs={(item.jmapAccountId && keywordDefsByAccount.get(item.jmapAccountId)) || folderKeywordDefs}
@@ -916,7 +927,7 @@ export default function EmailListScreen({
[
selectedIds, activeRowKey, selectionMode, handleRowPress, toggleSelect, swipeLeftAction, swipeRightAction,
swipeMode, handleRowSwipe, disableThreading, rowFlags, rowTagIds, threadCountFor,
showPreview, showRecipient, formatAddress, keywordDefsByAccount, folderKeywordDefs, inJunk, showAvatarsInJunk,
showPreview, showRecipientFor, formatAddress, keywordDefsByAccount, folderKeywordDefs, inJunk, showAvatarsInJunk,
loadAttachments, openAttachment,
],
);