mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 08:06:36 +00:00
fix: stop re-sorting the contact list on every search keystroke
Typing in the Contacts search re-ran the full sort on each keystroke, with `localeCompare` computing both display names and setting up a collator for every comparison. The list is now sorted once per contacts, category or sort-order change and search only filters the sorted list; the sort itself computes each sort name once and reuses a single `Intl.Collator` (PF10).
This commit is contained in:
@@ -50,8 +50,8 @@ interface Section {
|
||||
data: ContactCard[];
|
||||
}
|
||||
|
||||
function groupContacts(contacts: ContactCard[], byLastName: boolean): Section[] {
|
||||
const sorted = sortContactsByName(contacts, byLastName);
|
||||
/** Letter sections for contacts already in `sortContactsByName` order. */
|
||||
function groupContacts(sorted: ContactCard[], byLastName: boolean): Section[] {
|
||||
const groups: Record<string, ContactCard[]> = {};
|
||||
for (const c of sorted) {
|
||||
// Sections follow the sort name, so with "sort by last name" Alice Smith
|
||||
@@ -137,7 +137,7 @@ export default function ContactsScreen() {
|
||||
const groups = React.useMemo(() => contacts.filter(isGroup), [contacts]);
|
||||
const individuals = React.useMemo(() => contacts.filter((c) => !isGroup(c)), [contacts]);
|
||||
|
||||
const visible = React.useMemo(() => {
|
||||
const categoryContacts = React.useMemo(() => {
|
||||
let filtered: ContactCard[];
|
||||
switch (selectedCategory.type) {
|
||||
case 'all':
|
||||
@@ -162,14 +162,25 @@ export default function ContactsScreen() {
|
||||
default:
|
||||
filtered = individuals;
|
||||
}
|
||||
if (!searchQuery) return filtered;
|
||||
return filtered.filter((c) => matchesContactSearch(c, searchQuery));
|
||||
}, [contacts, individuals, selectedCategory, searchQuery]);
|
||||
return filtered;
|
||||
}, [contacts, individuals, selectedCategory]);
|
||||
|
||||
// Sort once per list or order change; search only filters the sorted list,
|
||||
// so typing never re-sorts the address book (PF10).
|
||||
const sorted = React.useMemo(
|
||||
() => sortContactsByName(categoryContacts, sortByLastName),
|
||||
[categoryContacts, sortByLastName],
|
||||
);
|
||||
|
||||
const visible = React.useMemo(() => {
|
||||
if (!searchQuery) return sorted;
|
||||
return sorted.filter((c) => matchesContactSearch(c, searchQuery));
|
||||
}, [sorted, searchQuery]);
|
||||
|
||||
const sections = React.useMemo<Section[]>(() => {
|
||||
if (groupByLetter) return groupContacts(visible, sortByLastName);
|
||||
// Flat list - single unnamed section keeps SectionList rendering simple.
|
||||
return [{ title: '', data: sortContactsByName(visible, sortByLastName) }];
|
||||
return [{ title: '', data: visible }];
|
||||
}, [visible, groupByLetter, sortByLastName]);
|
||||
|
||||
const selectionMode = selection.size > 0;
|
||||
|
||||
@@ -526,4 +526,10 @@ describe('sortContactsByName (#963)', () => {
|
||||
sortContactsByName(input, true);
|
||||
expect(input.map((c) => c.id)).toEqual(['zoe', 'alice', 'bob', 'acme']);
|
||||
});
|
||||
|
||||
it('compares case- and accent-insensitively, keeping ties in list order (PF10)', () => {
|
||||
const named = (id: string, full: string) => card(id, { name: { full } });
|
||||
const list = [named('z', 'Zed'), named('e2', 'emile'), named('e1', 'Émile'), named('a', 'Anna')];
|
||||
expect(sortContactsByName(list, false).map((c) => c.id)).toEqual(['a', 'e2', 'e1', 'z']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -928,9 +928,14 @@ export function sortContactsByDisplayName(contacts: ContactCard[]): ContactCard[
|
||||
return sortContactsByName(contacts, false);
|
||||
}
|
||||
|
||||
// One collator for every contact sort: `localeCompare` with options sets one
|
||||
// up per comparison, which made a large address book slow to sort (PF10).
|
||||
const contactNameCollator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
|
||||
/** Contact list order: by display name, or surname first with `byLastName` (#963). */
|
||||
export function sortContactsByName(contacts: ContactCard[], byLastName: boolean): ContactCard[] {
|
||||
return [...contacts].sort((a, b) =>
|
||||
getContactSortName(a, byLastName).localeCompare(getContactSortName(b, byLastName), undefined, { sensitivity: 'base' }),
|
||||
);
|
||||
// Work out each sort name once instead of twice per comparison.
|
||||
const keyed = contacts.map((contact) => ({ contact, key: getContactSortName(contact, byLastName) }));
|
||||
keyed.sort((a, b) => contactNameCollator.compare(a.key, b.key));
|
||||
return keyed.map((entry) => entry.contact);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user