From b73e8442b402ea693532483d1a5dffb1b3e233bb Mon Sep 17 00:00:00 2001 From: Matthew Grainger Date: Fri, 15 Nov 2024 16:01:30 -0500 Subject: [PATCH] feat: added duplicate contact warning icon in contact list --- app/_locales/en/messages.json | 3 + .../contact-list/contact-list.component.js | 41 +++++-- .../recipient-group.component.js | 3 +- ui/components/app/contact-list/utils.ts | 27 ++++- .../address-list-item.test.tsx.snap | 102 +++++++++++++++++- .../address-list-item.test.tsx | 38 +++++-- .../address-list-item/address-list-item.tsx | 17 ++- .../multichain/address-list-item/index.scss | 5 + .../add-contact/add-contact.component.js | 2 +- .../edit-contact/edit-contact.component.js | 2 +- 10 files changed, 220 insertions(+), 20 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 74c33d90451c..28b531d55487 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1727,6 +1727,9 @@ "dropped": { "message": "Dropped" }, + "duplicateContactTooltip": { + "message": "This contact name collides with an existing account or contact" + }, "duplicateContactWarning": { "message": "You have duplicate contacts" }, diff --git a/ui/components/app/contact-list/contact-list.component.js b/ui/components/app/contact-list/contact-list.component.js index e524f89f68af..b7438f9fe195 100644 --- a/ui/components/app/contact-list/contact-list.component.js +++ b/ui/components/app/contact-list/contact-list.component.js @@ -4,7 +4,7 @@ import { sortBy } from 'lodash'; import Button from '../../ui/button'; import { BannerAlert, BannerAlertSeverity } from '../../component-library'; import RecipientGroup from './recipient-group/recipient-group.component'; -import { hasDuplicateContacts } from './utils'; +import { hasDuplicateContacts, buildDuplicateContactMap } from './utils'; export default class ContactList extends PureComponent { static propTypes = { @@ -62,15 +62,40 @@ export default class ContactList extends PureComponent { } renderAddressBook() { - const unsortedContactsByLetter = this.props - .searchForContacts() - .reduce((obj, contact) => { + const { + addressBook, + internalAccounts, + searchForContacts, + selectRecipient, + selectedAddress, + } = this.props; + + const duplicateContactMap = buildDuplicateContactMap( + addressBook, + internalAccounts, + ); + + const unsortedContactsByLetter = searchForContacts().reduce( + (obj, contact) => { const firstLetter = contact.name[0].toUpperCase(); + + const isDuplicate = + (duplicateContactMap.get(contact.name.trim().toLowerCase()) ?? []) + .length > 1; + return { ...obj, - [firstLetter]: [...(obj[firstLetter] || []), contact], + [firstLetter]: [ + ...(obj[firstLetter] || []), + { + ...contact, + isDuplicate, + }, + ], }; - }, {}); + }, + {}, + ); const letters = Object.keys(unsortedContactsByLetter).sort(); @@ -88,8 +113,8 @@ export default class ContactList extends PureComponent { key={`${letter}-contact-group`} label={letter} items={groupItems} - onSelect={this.props.selectRecipient} - selectedAddress={this.props.selectedAddress} + onSelect={selectRecipient} + selectedAddress={selectedAddress} /> )); } diff --git a/ui/components/app/contact-list/recipient-group/recipient-group.component.js b/ui/components/app/contact-list/recipient-group/recipient-group.component.js index 6bb0b4c30dd6..0788d29aaecd 100644 --- a/ui/components/app/contact-list/recipient-group/recipient-group.component.js +++ b/ui/components/app/contact-list/recipient-group/recipient-group.component.js @@ -7,12 +7,13 @@ export default function RecipientGroup({ items, onSelect }) { return null; } - return items.map(({ address, name }) => ( + return items.map(({ address, name, isDuplicate }) => ( onSelect(address, name)} key={address} + isDuplicate={isDuplicate} /> )); } diff --git a/ui/components/app/contact-list/utils.ts b/ui/components/app/contact-list/utils.ts index f21d7f450d6d..4254988e4af6 100644 --- a/ui/components/app/contact-list/utils.ts +++ b/ui/components/app/contact-list/utils.ts @@ -1,6 +1,31 @@ import { AddressBookEntry } from '@metamask/address-book-controller'; import { InternalAccount } from '@metamask/keyring-api'; +export const buildDuplicateContactMap = ( + addressBook: AddressBookEntry[], + internalAccounts: InternalAccount[], +) => { + const contactMap = new Map( + internalAccounts.map((account) => [ + account.metadata.name.trim().toLowerCase(), + [`account-id-${account.id}`], + ]), + ); + + addressBook.forEach((entry) => { + const { name, address } = entry; + + const sanitizedName = name.trim().toLowerCase(); + + const currentArray = contactMap.get(sanitizedName) ?? []; + currentArray.push(address); + + contactMap.set(sanitizedName, currentArray); + }); + + return contactMap; +}; + export const hasDuplicateContacts = ( addressBook: AddressBookEntry[], internalAccounts: InternalAccount[], @@ -32,5 +57,5 @@ export const isDuplicateContact = ( metadata.name.toLowerCase().trim() === newName.toLowerCase().trim(), ); - return !nameExistsInAddressBook && !nameExistsInAccountList; + return nameExistsInAddressBook || nameExistsInAccountList; }; diff --git a/ui/components/multichain/address-list-item/__snapshots__/address-list-item.test.tsx.snap b/ui/components/multichain/address-list-item/__snapshots__/address-list-item.test.tsx.snap index 8d840ba595ce..c3895c50d76a 100644 --- a/ui/components/multichain/address-list-item/__snapshots__/address-list-item.test.tsx.snap +++ b/ui/components/multichain/address-list-item/__snapshots__/address-list-item.test.tsx.snap @@ -1,6 +1,106 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AddressListItem renders the address and label 1`] = ` +exports[`AddressListItem displays duplicate contact warning icon 1`] = ` +
+ +
+`; + +exports[`AddressListItem renders the address and label without duplicate contact warning icon 1`] = `