diff --git a/ts/components/leftPane/LeftPaneComposeHelper.tsx b/ts/components/leftPane/LeftPaneComposeHelper.tsx index 931264d8db..917eeffee0 100644 --- a/ts/components/leftPane/LeftPaneComposeHelper.tsx +++ b/ts/components/leftPane/LeftPaneComposeHelper.tsx @@ -18,6 +18,7 @@ import { isFetchingByE164, } from '../../util/uuidFetchState'; import type { GroupListItemConversationType } from '../conversationList/GroupListItem'; +import { isProbablyAUsername } from '../../util/Username'; export type LeftPaneComposePropsType = { composeContacts: ReadonlyArray; @@ -136,15 +137,15 @@ export class LeftPaneComposeHelper extends LeftPaneHelper { describe('getUsernameFromSearch', () => { const { getUsernameFromSearch } = Username; - it('matches invalid username searches', () => { - assert.isUndefined(getUsernameFromSearch('us')); - assert.isUndefined(getUsernameFromSearch('123')); - }); - it('matches partial username searches without discriminator', () => { + assert.strictEqual(getUsernameFromSearch('u'), 'u.01'); + assert.strictEqual(getUsernameFromSearch('us'), 'us.01'); assert.strictEqual(getUsernameFromSearch('use'), 'use.01'); assert.strictEqual(getUsernameFromSearch('use.'), 'use.01'); }); @@ -25,6 +22,11 @@ describe('Username', () => { assert.strictEqual(getUsernameFromSearch('@user.01'), 'user.01'); }); + it('adds a 1 if discriminator is one digit', () => { + assert.strictEqual(getUsernameFromSearch('@user.0'), 'user.01'); + assert.strictEqual(getUsernameFromSearch('@user.2'), 'user.21'); + }); + it('matches valid username searches', () => { assert.strictEqual(getUsernameFromSearch('username.12'), 'username.12'); assert.strictEqual(getUsernameFromSearch('xyz.568'), 'xyz.568'); @@ -37,4 +39,39 @@ describe('Username', () => { assert.isUndefined(getUsernameFromSearch('+234234234233')); }); }); + + describe('probablyAUsername', () => { + const { isProbablyAUsername: probablyAUsername } = Username; + + it('returns true if it starts with @', () => { + assert.isTrue(probablyAUsername('@')); + assert.isTrue(probablyAUsername('@5551115555')); + assert.isTrue(probablyAUsername('@.324')); + }); + + it('returns true if it ends with a discriminator', () => { + assert.isTrue(probablyAUsername('someone.00')); + assert.isTrue(probablyAUsername('32423423.04')); + assert.isTrue(probablyAUsername('d.04')); + }); + + it('returns false if just a discriminator', () => { + assert.isFalse(probablyAUsername('.01')); + assert.isFalse(probablyAUsername('.99')); + }); + + it('returns false for normal searches', () => { + assert.isFalse(probablyAUsername('group')); + assert.isFalse(probablyAUsername('climbers')); + assert.isFalse(probablyAUsername('sarah')); + assert.isFalse(probablyAUsername('john')); + }); + + it('returns false for something that looks like a phone number', () => { + assert.isFalse(probablyAUsername('+')); + assert.isFalse(probablyAUsername('2223')); + assert.isFalse(probablyAUsername('+3')); + assert.isFalse(probablyAUsername('+234234234233')); + }); + }); }); diff --git a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts index 687e6db1a2..1d49e2c926 100644 --- a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts +++ b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts @@ -82,7 +82,7 @@ describe('LeftPaneComposeHelper', () => { ); }); - it('returns the number of contacts, number groups + 4 (for headers and username)', () => { + it('returns two if the search term looks like a username', () => { assert.strictEqual( new LeftPaneComposeHelper({ composeContacts: [getDefaultConversation(), getDefaultConversation()], @@ -92,6 +92,45 @@ describe('LeftPaneComposeHelper', () => { uuidFetchState: {}, username: 'someone.01', }).getRowCount(), + 2, + 'ends with discriminator' + ); + assert.strictEqual( + new LeftPaneComposeHelper({ + composeContacts: [getDefaultConversation(), getDefaultConversation()], + composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()], + regionCode: 'US', + searchTerm: '@someone', + uuidFetchState: {}, + username: 'someone', + }).getRowCount(), + 2, + 'starts with @' + ); + assert.strictEqual( + new LeftPaneComposeHelper({ + composeContacts: [getDefaultConversation(), getDefaultConversation()], + composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()], + regionCode: 'US', + searchTerm: '23432', + uuidFetchState: {}, + username: '23432', + }).getRowCount(), + 8, + "all numbers, so it doesn't look like a username" + ); + }); + + it('returns the number of contacts, number groups + 4 (for headers and username)', () => { + assert.strictEqual( + new LeftPaneComposeHelper({ + composeContacts: [getDefaultConversation(), getDefaultConversation()], + composeGroups: [getDefaultGroupListItem(), getDefaultGroupListItem()], + regionCode: 'US', + searchTerm: 'someone', + uuidFetchState: {}, + username: 'someone', + }).getRowCount(), 8 ); }); @@ -102,9 +141,9 @@ describe('LeftPaneComposeHelper', () => { composeContacts: [], composeGroups: [], regionCode: 'US', - searchTerm: 'foobar.01', + searchTerm: '5550101', uuidFetchState: {}, - username: 'foobar.01', + username: undefined, }).getRowCount(), 2 ); @@ -113,9 +152,9 @@ describe('LeftPaneComposeHelper', () => { composeContacts: [getDefaultConversation(), getDefaultConversation()], composeGroups: [], regionCode: 'US', - searchTerm: 'foobar.01', + searchTerm: '5550101', uuidFetchState: {}, - username: 'foobar.01', + username: undefined, }).getRowCount(), 5 ); @@ -124,9 +163,9 @@ describe('LeftPaneComposeHelper', () => { composeContacts: [getDefaultConversation(), getDefaultConversation()], composeGroups: [getDefaultGroupListItem()], regionCode: 'US', - searchTerm: 'foobar.01', + searchTerm: '5550101', uuidFetchState: {}, - username: 'foobar.01', + username: undefined, }).getRowCount(), 7 ); @@ -152,9 +191,9 @@ describe('LeftPaneComposeHelper', () => { composeContacts: [], composeGroups: [], regionCode: 'US', - searchTerm: 'someone.02', + searchTerm: 'someone', uuidFetchState: {}, - username: 'someone.02', + username: 'someone', }).getRowCount(), 2 ); diff --git a/ts/util/Username.ts b/ts/util/Username.ts index 225576cd59..eba8d135be 100644 --- a/ts/util/Username.ts +++ b/ts/util/Username.ts @@ -1,10 +1,7 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { usernames } from '@signalapp/libsignal-client'; - import * as RemoteConfig from '../RemoteConfig'; -import { getNickname } from '../types/Username'; import { parseIntWithFallback } from './parseIntWithFallback'; export function getMaxNickname(): number { @@ -17,29 +14,56 @@ export function getMinNickname(): number { return parseIntWithFallback(RemoteConfig.getValue('global.nicknames.min'), 3); } +const USERNAME_CHARS = /^@?[a-zA-Z0-9]+(.\d+)?$/; +const ALL_DIGITS = /^\d+$/; + export function getUsernameFromSearch(searchTerm: string): string | undefined { - const nickname = getNickname(searchTerm); - if (nickname == null || nickname.length < getMinNickname()) { + let modifiedTerm = searchTerm; + + if (ALL_DIGITS.test(searchTerm)) { return undefined; } - let modifiedTerm = searchTerm; - if (modifiedTerm.startsWith('@')) { modifiedTerm = modifiedTerm.slice(1); } if (modifiedTerm.endsWith('.')) { // Allow nicknames without full discriminator modifiedTerm = `${modifiedTerm}01`; + } else if (/\.\d$/.test(modifiedTerm)) { + // Add one more digit if they only have one + modifiedTerm = `${modifiedTerm}1`; } else if (!/\.\d*$/.test(modifiedTerm)) { // Allow nicknames without discriminator modifiedTerm = `${modifiedTerm}.01`; } + if (!USERNAME_CHARS.test(modifiedTerm)) { + return undefined; + } + try { - usernames.hash(modifiedTerm); return modifiedTerm; } catch { return undefined; } } + +export function isProbablyAUsername(searchTerm: string): boolean { + if (searchTerm.startsWith('@')) { + return true; + } + + if (!USERNAME_CHARS.test(searchTerm)) { + return false; + } + if (ALL_DIGITS.test(searchTerm)) { + return false; + } + + if (/.+\.\d\d\d?$/.test(searchTerm)) { + return true; + } + + return false; +}