From 681c029ded0567f4f64754a1a50f9dfe48083499 Mon Sep 17 00:00:00 2001 From: vansh-nagar Date: Mon, 28 Sep 2026 11:45:49 +0530 Subject: [PATCH] feat(ui): show addresses in search recipients --- src/components/SearchMessages.vue | 21 +++++++++++++-- src/tests/unit/util/emailAddress.spec.js | 34 +++++++++++++++++++++++- src/util/emailAddress.js | 19 +++++++++++++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/components/SearchMessages.vue b/src/components/SearchMessages.vue index fa8216d1ef..fb0a967576 100644 --- a/src/components/SearchMessages.vue +++ b/src/components/SearchMessages.vue @@ -113,7 +113,14 @@ :max="1" @option:selecting="addTag($event, 'from')" @option:deselecting="removeTag($event, 'from')" - @search="searchRecipients($event)" /> + @search="searchRecipients($event)"> + + + @@ -137,7 +144,14 @@ :preserve-search="true" @option:selecting="addTag($event, 'to')" @option:deselecting="removeTag($event, 'to')" - @search="searchRecipients($event)" /> + @search="searchRecipients($event)"> + + + @@ -315,6 +329,7 @@ import Close from 'vue-material-design-icons/Close.vue' import FilterVariantIcon from 'vue-material-design-icons/FilterVariant.vue' import { findRecipient } from '../service/AutocompleteService.js' import useMainStore from '../store/mainStore.js' +import { formatRecipient } from '../util/emailAddress.js' import { hiddenTags } from './tags.js' const debouncedSearch = debouncePromise(findRecipient, 500) @@ -492,6 +507,8 @@ export default { }, methods: { + formatRecipient, + hideButtonsWithDelay(delay = false) { if (delay) { setTimeout(() => { diff --git a/src/tests/unit/util/emailAddress.spec.js b/src/tests/unit/util/emailAddress.spec.js index 92a442db27..429e4dc532 100644 --- a/src/tests/unit/util/emailAddress.spec.js +++ b/src/tests/unit/util/emailAddress.spec.js @@ -3,7 +3,39 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { getLabelAndAddress, parseEmailList } from '../../../util/emailAddress.js' +import { formatRecipient, getLabelAndAddress, parseEmailList } from '../../../util/emailAddress.js' + +describe('formatRecipient', () => { + it('shows the email next to a distinct display name', () => { + expect(formatRecipient({ + label: 'Alice Smith', + email: 'alice@example.com', + source: 'contacts', + })).toBe('Alice Smith (alice@example.com)') + }) + + it('does not repeat the email when it is also the label', () => { + expect(formatRecipient({ + label: 'alice@example.com', + email: 'alice@example.com', + source: 'contacts', + })).toBe('alice@example.com') + }) + + it('does not format group identifiers as email addresses', () => { + expect(formatRecipient({ + label: 'Journalists', + email: 'group-id', + source: 'groups', + })).toBe('Journalists') + }) + + it('falls back to the available recipient value', () => { + expect(formatRecipient({ email: 'alice@example.com' })).toBe('alice@example.com') + expect(formatRecipient({ label: 'Alice Smith' })).toBe('Alice Smith') + expect(formatRecipient(undefined)).toBe('') + }) +}) describe('getLabelAndAddress', () => { it('parses a plain email address', () => { diff --git a/src/util/emailAddress.js b/src/util/emailAddress.js index 8a6ed6e0b1..b01e65a39f 100644 --- a/src/util/emailAddress.js +++ b/src/util/emailAddress.js @@ -80,6 +80,25 @@ export function getLabelAndAddress(str) { return results.length > 0 ? results[0] : null } +/** + * Format a recipient for display without hiding their email address. + * + * @param {{ label?: string, email?: string, source?: string } | null | undefined} recipient The recipient + * @return {string} The display value + */ +export function formatRecipient(recipient) { + if (!recipient) { + return '' + } + + const label = recipient.label || recipient.email || '' + if (recipient.source === 'contacts' && recipient.email && recipient.email !== label) { + return `${label} (${recipient.email})` + } + + return label +} + /** * Parse a string containing one or more email addresses separated by * commas or semicolons, with limited support for spaces between bare