diff --git a/package.json b/package.json index e714e330f33..144c68c9788 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@react-navigation/native": "^5.7.6", "@react-navigation/stack": "^5.9.3", "@sentry/react-native": "^3.1.1", - "@zulip/shared": "^0.0.8", + "@zulip/shared": "^0.0.9", "base-64": "^1.0.0", "blueimp-md5": "^2.10.0", "color": "^4.0.1", diff --git a/src/users/__tests__/userHelpers-test.js b/src/users/__tests__/userHelpers-test.js index 9ef2b6afb1d..eb2ca232372 100644 --- a/src/users/__tests__/userHelpers-test.js +++ b/src/users/__tests__/userHelpers-test.js @@ -9,7 +9,6 @@ import { getAutocompleteUserGroupSuggestions, sortAlphabetically, filterUserStartWith, - filterUserByInitials, filterUserThatContains, filterUserMatchesEmail, getUniqueUsers, @@ -115,8 +114,8 @@ describe('getAutocompleteSuggestion', () => { expect(filteredUsers).toEqual(shouldMatch); }); - test('result should be in priority of startsWith, initials, contains in name, matches in email', () => { - const user1 = eg.makeUser({ name: 'M Apple', email: 'any1@example.com' }); // satisfy initials condition + test('result should be in priority of startsWith, contains in name, matches in email', () => { + const user1 = eg.makeUser({ name: 'M Apple', email: 'any1@example.com' }); // does not match const user2 = eg.makeUser({ name: 'Normal boy', email: 'any2@example.com' }); // satisfy full_name contains condition const user3 = eg.makeUser({ name: 'example', email: 'example@example.com' }); // random entry const user4 = eg.makeUser({ name: 'Example', email: 'match@example.com' }); // satisfy email match condition @@ -125,7 +124,7 @@ describe('getAutocompleteSuggestion', () => { const user7 = eg.makeUser({ name: 'Match App Normal', email: 'any3@example.com' }); // satisfy all conditions const user8 = eg.makeUser({ name: 'match', email: 'any@example.com' }); // duplicate const user9 = eg.makeUser({ name: 'Laptop', email: 'laptop@example.com' }); // random entry - const user10 = eg.makeUser({ name: 'Mobile App', email: 'any@match.com' }); // satisfy initials and email condition + const user10 = eg.makeUser({ name: 'Mobile App', email: 'any@match.com' }); // satisfy email condition const user11 = eg.makeUser({ name: 'Normal', email: 'match2@example.com' }); // satisfy contains in name and matches in email condition const allUsers = deepFreeze([ user1, @@ -145,11 +144,10 @@ describe('getAutocompleteSuggestion', () => { user5, // name starts with 'ma' user6, // have priority as starts with 'ma' user7, // have priority as starts with 'ma' - user1, // initials 'MA' - user10, // have priority because of initials condition user2, // name contains in 'ma' user11, // have priority because of 'ma' contains in name user4, // email contains 'ma' + user10, // email contains 'ma' ]; const filteredUsers = getAutocompleteSuggestion( allUsers, @@ -259,22 +257,31 @@ describe('filterUserStartWith', () => { const expectedUsers = [user1, user3]; expect(filterUserStartWith(users, 'app', selfUser.user_id)).toEqual(expectedUsers); }); -}); - -describe('filterUserByInitials', () => { - test('returns users whose full_name initials matches filter excluding self', () => { - const user1 = eg.makeUser({ name: 'Apple', email: 'a@example.com' }); - const user2 = eg.makeUser({ name: 'mam', email: 'f@app.com' }); - const user3 = eg.makeUser({ name: 'app', email: 'p@p.com' }); - const user4 = eg.makeUser({ name: 'Mobile Application', email: 'p3@p.com' }); - const user5 = eg.makeUser({ name: 'Mac App', email: 'p@p2.com' }); - const user6 = eg.makeUser({ name: 'app', email: 'p@p.com' }); - const selfUser = eg.makeUser({ name: 'app', email: 'own@example.com' }); - const users = deepFreeze([user1, user2, user3, user4, user5, user6, selfUser]); + test('returns users whose name contains diacritics but otherwise starts with filter', () => { + const withDiacritics = eg.makeUser({ name: 'Frödö', email: 'bagginsf@example.com' }); + const withoutDiacritics = eg.makeUser({ name: 'Frodo', email: 'bagginz@example.com' }); + const nonMatchingUser = eg.makeUser({ name: 'Zalix', email: 'zalix@example.com' }); + const users = deepFreeze([withDiacritics, withoutDiacritics, nonMatchingUser]); + const expectedUsers = [withDiacritics, withoutDiacritics]; + expect(filterUserStartWith(users, 'Fro', eg.makeUser().user_id)).toEqual(expectedUsers); + }); - const expectedUsers = [user4, user5]; - expect(filterUserByInitials(users, 'ma', selfUser.user_id)).toEqual(expectedUsers); + test('returns users whose name contains diacritics and filter uses diacritics', () => { + const withDiacritics = eg.makeUser({ name: 'Frödö', email: 'bagginsf@example.com' }); + const withoutDiacritics = eg.makeUser({ name: 'Frodo', email: 'bagginz@example.com' }); + const wrongDiacritics = eg.makeUser({ name: 'Frōdō', email: 'baggins@example.com' }); + const notIncludedDiactritic = eg.makeUser({ name: 'Fřödo', email: 'baggins@example.com' }); + const nonMatchingUser = eg.makeUser({ name: 'Zalix', email: 'zalix@example.com' }); + const users = deepFreeze([ + withDiacritics, + withoutDiacritics, + wrongDiacritics, + notIncludedDiactritic, + nonMatchingUser, + ]); + const expectedUsers = [withDiacritics]; + expect(filterUserStartWith(users, 'Frö', eg.makeUser().user_id)).toEqual(expectedUsers); }); }); @@ -331,6 +338,32 @@ describe('filterUserThatContains', () => { const expectedUsers = [user2, user5]; expect(filterUserThatContains(users, 'ma', selfUser.user_id)).toEqual(expectedUsers); }); + + test('returns users whose full_name has diacritics but otherwise contains filter', () => { + const withDiacritics = eg.makeUser({ name: 'Aärdvärk', email: 'aardvark@example.com' }); + const withoutDiacritics = eg.makeUser({ name: 'Aardvark', email: 'ard@example.com' }); + const nonMatchingUser = eg.makeUser({ name: 'Turtle', email: 'turtle@example.com' }); + const users = deepFreeze([withDiacritics, withoutDiacritics, nonMatchingUser]); + const expectedUsers = [withDiacritics, withoutDiacritics]; + expect(filterUserThatContains(users, 'vark', eg.makeUser().user_id)).toEqual(expectedUsers); + }); + + test('returns users whose full_name has diacritics and filter uses diacritics', () => { + const withDiacritics = eg.makeUser({ name: 'Aärdvärk', email: 'aardvark@example.com' }); + const withoutDiacritics = eg.makeUser({ name: 'Aardvark', email: 'aardvark@example.com' }); + const wrongDiacritics = eg.makeUser({ name: 'Aärdvãrk', email: 'aadvark@example.com' }); + const notIncludedDiactritic = eg.makeUser({ name: 'Aärdväŕk', email: 'aadvark@example.com' }); + const nonMatchingUser = eg.makeUser({ name: 'Turtle', email: 'turtle@example.com' }); + const users = deepFreeze([ + withDiacritics, + withoutDiacritics, + wrongDiacritics, + notIncludedDiactritic, + nonMatchingUser, + ]); + const expectedUsers = [withDiacritics]; + expect(filterUserThatContains(users, 'värk', eg.makeUser().user_id)).toEqual(expectedUsers); + }); }); describe('filterUserMatchesEmail', () => { diff --git a/src/users/userHelpers.js b/src/users/userHelpers.js index ca71a615d01..c0e01b4f2d8 100644 --- a/src/users/userHelpers.js +++ b/src/users/userHelpers.js @@ -1,6 +1,7 @@ /* @flow strict-local */ // $FlowFixMe[untyped-import] import uniqby from 'lodash.uniqby'; +import * as typeahead from '@zulip/shared/js/typeahead'; import type { MutedUsersState, @@ -86,35 +87,27 @@ export const filterUserStartWith = ( users: $ReadOnlyArray, filter: string = '', ownUserId: UserId, -): $ReadOnlyArray => - users.filter( - user => - user.user_id !== ownUserId && user.full_name.toLowerCase().startsWith(filter.toLowerCase()), - ); - -export const filterUserByInitials = ( - users: $ReadOnlyArray, - filter: string = '', - ownUserId: UserId, -): $ReadOnlyArray => - users.filter( - user => - user.user_id !== ownUserId - && user.full_name - .replace(/(\s|[a-z])/g, '') - .toLowerCase() - .startsWith(filter.toLowerCase()), - ); +): $ReadOnlyArray => { + const loweredFilter = filter.toLowerCase(); + const isAscii = /^[a-z]+$/.test(loweredFilter); + return users.filter(user => { + const full_name = isAscii ? typeahead.remove_diacritics(user.full_name) : user.full_name; + return user.user_id !== ownUserId && full_name.toLowerCase().startsWith(loweredFilter); + }); +}; export const filterUserThatContains = ( users: $ReadOnlyArray, filter: string = '', ownUserId: UserId, -): $ReadOnlyArray => - users.filter( - user => - user.user_id !== ownUserId && user.full_name.toLowerCase().includes(filter.toLowerCase()), - ); +): $ReadOnlyArray => { + const loweredFilter = filter.toLowerCase(); + const isAscii = /^[a-z]+$/.test(loweredFilter); + return users.filter(user => { + const full_name = isAscii ? typeahead.remove_diacritics(user.full_name) : user.full_name; + return user.user_id !== ownUserId && full_name.toLowerCase().includes(filter.toLowerCase()); + }); +}; export const filterUserMatchesEmail = ( users: $ReadOnlyArray, @@ -150,10 +143,9 @@ export const getAutocompleteSuggestion = ( } const allAutocompleteOptions = getUsersAndWildcards(users); const startWith = filterUserStartWith(allAutocompleteOptions, filter, ownUserId); - const initials = filterUserByInitials(allAutocompleteOptions, filter, ownUserId); const contains = filterUserThatContains(allAutocompleteOptions, filter, ownUserId); const matchesEmail = filterUserMatchesEmail(users, filter, ownUserId); - const candidates = getUniqueUsers([...startWith, ...initials, ...contains, ...matchesEmail]); + const candidates = getUniqueUsers([...startWith, ...contains, ...matchesEmail]); return candidates.filter(user => !mutedUsers.has(user.user_id)); }; diff --git a/yarn.lock b/yarn.lock index 52b6cd43a08..f14e38feedd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2586,10 +2586,10 @@ resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -"@zulip/shared@^0.0.8": - version "0.0.8" - resolved "https://registry.yarnpkg.com/@zulip/shared/-/shared-0.0.8.tgz#c0a786eb09c30cae3da364dee4bd49a09529129e" - integrity sha512-SmFzU2a2gqt3+XDdu7UF1/5iS/1t6Gvh/Qc9gWdN/pcVzX4qr4l0di6toORSeq/rX2FiHBANrOdozmRXLgRpHw== +"@zulip/shared@^0.0.9": + version "0.0.9" + resolved "https://registry.yarnpkg.com/@zulip/shared/-/shared-0.0.9.tgz#a7bb13eb22097e0c0f9e1c96008a007860b495e3" + integrity sha512-DYQ2pEUcDgLkMvoltnO4EHI1G/tKuuHpKk11gual6Nwih5ZRfcHLOzb40wBfkwwrj3tgEDdniWcggmQnyEseNw== dependencies: katex "^0.12.0" lodash "^4.17.19"