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[];
|
data: ContactCard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupContacts(contacts: ContactCard[], byLastName: boolean): Section[] {
|
/** Letter sections for contacts already in `sortContactsByName` order. */
|
||||||
const sorted = sortContactsByName(contacts, byLastName);
|
function groupContacts(sorted: ContactCard[], byLastName: boolean): Section[] {
|
||||||
const groups: Record<string, ContactCard[]> = {};
|
const groups: Record<string, ContactCard[]> = {};
|
||||||
for (const c of sorted) {
|
for (const c of sorted) {
|
||||||
// Sections follow the sort name, so with "sort by last name" Alice Smith
|
// 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 groups = React.useMemo(() => contacts.filter(isGroup), [contacts]);
|
||||||
const individuals = React.useMemo(() => contacts.filter((c) => !isGroup(c)), [contacts]);
|
const individuals = React.useMemo(() => contacts.filter((c) => !isGroup(c)), [contacts]);
|
||||||
|
|
||||||
const visible = React.useMemo(() => {
|
const categoryContacts = React.useMemo(() => {
|
||||||
let filtered: ContactCard[];
|
let filtered: ContactCard[];
|
||||||
switch (selectedCategory.type) {
|
switch (selectedCategory.type) {
|
||||||
case 'all':
|
case 'all':
|
||||||
@@ -162,14 +162,25 @@ export default function ContactsScreen() {
|
|||||||
default:
|
default:
|
||||||
filtered = individuals;
|
filtered = individuals;
|
||||||
}
|
}
|
||||||
if (!searchQuery) return filtered;
|
return filtered;
|
||||||
return filtered.filter((c) => matchesContactSearch(c, searchQuery));
|
}, [contacts, individuals, selectedCategory]);
|
||||||
}, [contacts, individuals, selectedCategory, searchQuery]);
|
|
||||||
|
// 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[]>(() => {
|
const sections = React.useMemo<Section[]>(() => {
|
||||||
if (groupByLetter) return groupContacts(visible, sortByLastName);
|
if (groupByLetter) return groupContacts(visible, sortByLastName);
|
||||||
// Flat list - single unnamed section keeps SectionList rendering simple.
|
// Flat list - single unnamed section keeps SectionList rendering simple.
|
||||||
return [{ title: '', data: sortContactsByName(visible, sortByLastName) }];
|
return [{ title: '', data: visible }];
|
||||||
}, [visible, groupByLetter, sortByLastName]);
|
}, [visible, groupByLetter, sortByLastName]);
|
||||||
|
|
||||||
const selectionMode = selection.size > 0;
|
const selectionMode = selection.size > 0;
|
||||||
|
|||||||
@@ -526,4 +526,10 @@ describe('sortContactsByName (#963)', () => {
|
|||||||
sortContactsByName(input, true);
|
sortContactsByName(input, true);
|
||||||
expect(input.map((c) => c.id)).toEqual(['zoe', 'alice', 'bob', 'acme']);
|
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);
|
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). */
|
/** Contact list order: by display name, or surname first with `byLastName` (#963). */
|
||||||
export function sortContactsByName(contacts: ContactCard[], byLastName: boolean): ContactCard[] {
|
export function sortContactsByName(contacts: ContactCard[], byLastName: boolean): ContactCard[] {
|
||||||
return [...contacts].sort((a, b) =>
|
// Work out each sort name once instead of twice per comparison.
|
||||||
getContactSortName(a, byLastName).localeCompare(getContactSortName(b, byLastName), undefined, { sensitivity: 'base' }),
|
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