Skip to content

Commit 7bb3486

Browse files
committed
typeahead: Ignore diacritics when filtering users.
When invoking the typeahead filter for users it is desirable to be able to see results for users with diacritics in their names even if the filter does not contain the matching diacritics. This PR leverages @zulip/shared typeahead module in order to strip diacritics out of the filtering process, unless the user explicitly uses them in the filter. Fixes: #3710
1 parent 0a07414 commit 7bb3486

File tree

2 files changed

+56
-13
lines changed

2 files changed

+56
-13
lines changed

src/users/__tests__/userHelpers-test.js

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe('getAutocompleteSuggestion', () => {
115115
});
116116

117117
test('result should be in priority of startsWith, contains in name, matches in email', () => {
118-
const user1 = eg.makeUser({ name: 'M Apple', email: '[email protected]' }); // satisfy initials condition
118+
const user1 = eg.makeUser({ name: 'M Apple', email: '[email protected]' }); // does not match
119119
const user2 = eg.makeUser({ name: 'Normal boy', email: '[email protected]' }); // satisfy full_name contains condition
120120
const user3 = eg.makeUser({ name: 'example', email: '[email protected]' }); // random entry
121121
const user4 = eg.makeUser({ name: 'Example', email: '[email protected]' }); // satisfy email match condition
@@ -124,7 +124,7 @@ describe('getAutocompleteSuggestion', () => {
124124
const user7 = eg.makeUser({ name: 'Match App Normal', email: '[email protected]' }); // satisfy all conditions
125125
const user8 = eg.makeUser({ name: 'match', email: '[email protected]' }); // duplicate
126126
const user9 = eg.makeUser({ name: 'Laptop', email: '[email protected]' }); // random entry
127-
const user10 = eg.makeUser({ name: 'Mobile App', email: '[email protected]' }); // satisfy initials and email condition
127+
const user10 = eg.makeUser({ name: 'Mobile App', email: '[email protected]' }); // satisfy email condition
128128
const user11 = eg.makeUser({ name: 'Normal', email: '[email protected]' }); // satisfy contains in name and matches in email condition
129129
const allUsers = deepFreeze([
130130
user1,
@@ -147,7 +147,7 @@ describe('getAutocompleteSuggestion', () => {
147147
user2, // name contains in 'ma'
148148
user11, // have priority because of 'ma' contains in name
149149
user4, // email contains 'ma'
150-
user10, // have priority because of initials condition
150+
user10, // email contains 'ma'
151151
];
152152
const filteredUsers = getAutocompleteSuggestion(
153153
allUsers,
@@ -257,6 +257,24 @@ describe('filterUserStartWith', () => {
257257
const expectedUsers = [user1, user3];
258258
expect(filterUserStartWith(users, 'app', selfUser.user_id)).toEqual(expectedUsers);
259259
});
260+
261+
test('returns users whose name contains diacritics but otherwise starts with filter', () => {
262+
const withDiacritics = eg.makeUser({ name: 'Frödö', email: '[email protected]' });
263+
const withoutDiacritics = eg.makeUser({ name: 'Frodo', email: '[email protected]' });
264+
const nonMatchingUser = eg.makeUser({ name: 'Zalix', email: '[email protected]' });
265+
const users = deepFreeze([withDiacritics, withoutDiacritics, nonMatchingUser]);
266+
const expectedUsers = [withDiacritics, withoutDiacritics];
267+
expect(filterUserStartWith(users, 'Fro', eg.makeUser().user_id)).toEqual(expectedUsers);
268+
});
269+
270+
test('returns users whose name contains diacritics and filter uses diacritics', () => {
271+
const withDiacritics = eg.makeUser({ name: 'Frödö', email: '[email protected]' });
272+
const withoutDiacritics = eg.makeUser({ name: 'Frodo', email: '[email protected]' });
273+
const nonMatchingUser = eg.makeUser({ name: 'Zalix', email: '[email protected]' });
274+
const users = deepFreeze([withDiacritics, withoutDiacritics, nonMatchingUser]);
275+
const expectedUsers = [withDiacritics];
276+
expect(filterUserStartWith(users, 'Frö', eg.makeUser().user_id)).toEqual(expectedUsers);
277+
});
260278
});
261279

262280
describe('groupUsersByStatus', () => {
@@ -312,6 +330,24 @@ describe('filterUserThatContains', () => {
312330
const expectedUsers = [user2, user5];
313331
expect(filterUserThatContains(users, 'ma', selfUser.user_id)).toEqual(expectedUsers);
314332
});
333+
334+
test('returns users whose full_name has diacritics but otherwise contains filter', () => {
335+
const withDiacritics = eg.makeUser({ name: 'Aärdvärk', email: '[email protected]' });
336+
const withoutDiacritics = eg.makeUser({ name: 'Aardvark', email: '[email protected]' });
337+
const nonMatchingUser = eg.makeUser({ name: 'Turtle', email: '[email protected]' });
338+
const users = deepFreeze([withDiacritics, withoutDiacritics, nonMatchingUser]);
339+
const expectedUsers = [withDiacritics, withoutDiacritics];
340+
expect(filterUserThatContains(users, 'vark', eg.makeUser().user_id)).toEqual(expectedUsers);
341+
});
342+
343+
test('returns users whose full_name has diacritics and filter uses diacritics', () => {
344+
const withDiacritics = eg.makeUser({ name: 'Aärdvärk', email: '[email protected]' });
345+
const withoutDiacritics = eg.makeUser({ name: 'Aardvark', email: '[email protected]' });
346+
const nonMatchingUser = eg.makeUser({ name: 'Turtle', email: '[email protected]' });
347+
const users = deepFreeze([withDiacritics, withoutDiacritics, nonMatchingUser]);
348+
const expectedUsers = [withDiacritics];
349+
expect(filterUserThatContains(users, 'värk', eg.makeUser().user_id)).toEqual(expectedUsers);
350+
});
315351
});
316352

317353
describe('filterUserMatchesEmail', () => {

src/users/userHelpers.js

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* @flow strict-local */
22
// $FlowFixMe[untyped-import]
33
import uniqby from 'lodash.uniqby';
4+
import * as typeahead from '@zulip/shared/js/typeahead';
45

56
import type {
67
MutedUsersState,
@@ -86,21 +87,27 @@ export const filterUserStartWith = (
8687
users: $ReadOnlyArray<AutocompleteOption>,
8788
filter: string = '',
8889
ownUserId: UserId,
89-
): $ReadOnlyArray<AutocompleteOption> =>
90-
users.filter(
91-
user =>
92-
user.user_id !== ownUserId && user.full_name.toLowerCase().startsWith(filter.toLowerCase()),
93-
);
90+
): $ReadOnlyArray<AutocompleteOption> => {
91+
const loweredFilter = filter.toLowerCase();
92+
const isAscii = /^[a-z]+$/.test(loweredFilter);
93+
return users.filter(user => {
94+
const full_name = isAscii ? typeahead.remove_diacritics(user.full_name) : user.full_name;
95+
return user.user_id !== ownUserId && full_name.toLowerCase().startsWith(loweredFilter);
96+
});
97+
};
9498

9599
export const filterUserThatContains = (
96100
users: $ReadOnlyArray<AutocompleteOption>,
97101
filter: string = '',
98102
ownUserId: UserId,
99-
): $ReadOnlyArray<AutocompleteOption> =>
100-
users.filter(
101-
user =>
102-
user.user_id !== ownUserId && user.full_name.toLowerCase().includes(filter.toLowerCase()),
103-
);
103+
): $ReadOnlyArray<AutocompleteOption> => {
104+
const loweredFilter = filter.toLowerCase();
105+
const isAscii = /^[a-z]+$/.test(loweredFilter);
106+
return users.filter(user => {
107+
const full_name = isAscii ? typeahead.remove_diacritics(user.full_name) : user.full_name;
108+
return user.user_id !== ownUserId && full_name.toLowerCase().includes(filter.toLowerCase());
109+
});
110+
};
104111

105112
export const filterUserMatchesEmail = (
106113
users: $ReadOnlyArray<AutocompleteOption>,

0 commit comments

Comments
 (0)