mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-20 02:08:57 +00:00
Find by username: Don't automatically add .01 discriminator
This commit is contained in:
@@ -2866,7 +2866,11 @@
|
||||
},
|
||||
"icu:startConversation--username-not-found": {
|
||||
"messageformat": "{atUsername} is not a Signal user. Make sure you've entered the complete username.",
|
||||
"description": "Shown in dialog if username is not found. Note that 'username' will be the output of at-username"
|
||||
"description": "Shown in dialog if username is not found. Note that 'username' will be the output of at-username, no @ symbol"
|
||||
},
|
||||
"icu:startConversation--username-not-valid": {
|
||||
"messageformat": "{atUsername} is not a valid username. Make sure you've entered the complete username followed by its set of digits.",
|
||||
"description": "Shown in dialog if username is invalid. Note that 'username' will be the output of at-username, no @ symbol"
|
||||
},
|
||||
"icu:startConversation--phone-number-not-found": {
|
||||
"messageformat": "User not found. \"{phoneNumber}\" is not a Signal user.",
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import type { SmartDraftGifMessageSendModalProps } from '../state/smart/DraftGifMessageSendModal.preload.js';
|
||||
import { CriticalIdlePrimaryDeviceModal } from './CriticalIdlePrimaryDeviceModal.dom.js';
|
||||
import { LowDiskSpaceBackupImportModal } from './LowDiskSpaceBackupImportModal.dom.js';
|
||||
import { isUsernameValid } from '../util/Username.dom.js';
|
||||
|
||||
// NOTE: All types should be required for this component so that the smart
|
||||
// component gives you type errors when adding/removing props.
|
||||
@@ -392,6 +393,13 @@ export function GlobalModalContainer({
|
||||
content = i18n('icu:startConversation--phone-number-not-found', {
|
||||
phoneNumber: userNotFoundModalState.phoneNumber,
|
||||
});
|
||||
} else if (
|
||||
userNotFoundModalState.type === 'username' &&
|
||||
!isUsernameValid(userNotFoundModalState.username)
|
||||
) {
|
||||
content = i18n('icu:startConversation--username-not-valid', {
|
||||
atUsername: userNotFoundModalState.username,
|
||||
});
|
||||
} else if (userNotFoundModalState.type === 'username') {
|
||||
content = i18n('icu:startConversation--username-not-found', {
|
||||
atUsername: userNotFoundModalState.username,
|
||||
|
||||
@@ -6,27 +6,68 @@ import { assert } from 'chai';
|
||||
import * as Username from '../../util/Username.dom.js';
|
||||
|
||||
describe('Username', () => {
|
||||
describe('isUsernameValid', () => {
|
||||
const { isUsernameValid } = Username;
|
||||
|
||||
it('returns false for missing discriminator', () => {
|
||||
assert.isFalse(isUsernameValid('use'));
|
||||
assert.isFalse(isUsernameValid('user'));
|
||||
assert.isFalse(isUsernameValid('usern'));
|
||||
assert.isFalse(isUsernameValid('usern.'));
|
||||
});
|
||||
|
||||
it('matches valid username searches', () => {
|
||||
assert.isTrue(isUsernameValid('username.01'));
|
||||
assert.isTrue(isUsernameValid('username.12'));
|
||||
assert.isTrue(isUsernameValid('xyz.568'));
|
||||
assert.isTrue(isUsernameValid('numbered9.34'));
|
||||
assert.isTrue(isUsernameValid('u12.34'));
|
||||
assert.isTrue(isUsernameValid('with_underscore.56'));
|
||||
assert.isTrue(isUsernameValid('username_with_32_characters_1234.45'));
|
||||
});
|
||||
|
||||
it('does not match when then username starts with a number', () => {
|
||||
assert.isFalse(isUsernameValid('1user.12'));
|
||||
assert.isFalse(isUsernameValid('9user_name.12'));
|
||||
});
|
||||
|
||||
it('does not match usernames shorter than 3 characters or longer than 32', () => {
|
||||
assert.isFalse(isUsernameValid('us.12'));
|
||||
assert.isFalse(isUsernameValid('username_with_33_characters_12345.67'));
|
||||
});
|
||||
|
||||
it('does not match something that looks like a phone number', () => {
|
||||
assert.isFalse(isUsernameValid('+'));
|
||||
assert.isFalse(isUsernameValid('2223'));
|
||||
assert.isFalse(isUsernameValid('+3'));
|
||||
assert.isFalse(isUsernameValid('+234234234233'));
|
||||
});
|
||||
|
||||
it('does not match invalid discriminators', () => {
|
||||
assert.isFalse(isUsernameValid('username.0'));
|
||||
assert.isFalse(isUsernameValid('username.00'));
|
||||
assert.isFalse(isUsernameValid('username.000'));
|
||||
assert.isFalse(isUsernameValid('username.001'));
|
||||
assert.isFalse(isUsernameValid('username.012'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUsernameFromSearch', () => {
|
||||
const { getUsernameFromSearch } = Username;
|
||||
|
||||
it('matches partial username searches without discriminator', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('use'), 'use.01');
|
||||
assert.strictEqual(getUsernameFromSearch('user'), 'user.01');
|
||||
assert.strictEqual(getUsernameFromSearch('usern'), 'usern.01');
|
||||
assert.strictEqual(getUsernameFromSearch('usern.'), 'usern.01');
|
||||
assert.strictEqual(getUsernameFromSearch('use'), 'use');
|
||||
assert.strictEqual(getUsernameFromSearch('user'), 'user');
|
||||
assert.strictEqual(getUsernameFromSearch('usern'), 'usern');
|
||||
assert.strictEqual(getUsernameFromSearch('usern.'), 'usern.');
|
||||
});
|
||||
|
||||
it('matches and strips leading @', () => {
|
||||
assert.strictEqual(getUsernameFromSearch('@user'), 'user.01');
|
||||
assert.strictEqual(getUsernameFromSearch('@user.'), 'user.01');
|
||||
assert.strictEqual(getUsernameFromSearch('@user'), 'user');
|
||||
assert.strictEqual(getUsernameFromSearch('@user.'), 'user.');
|
||||
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');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as RemoteConfig from '../RemoteConfig.dom.js';
|
||||
import { getDiscriminator, getNickname } from '../types/Username.std.js';
|
||||
import { parseIntWithFallback } from './parseIntWithFallback.std.js';
|
||||
|
||||
export function getMaxNickname(): number {
|
||||
@@ -15,9 +16,42 @@ export function getMinNickname(): number {
|
||||
}
|
||||
|
||||
// Usernames have a minimum length of 3 and maximum of 32
|
||||
const USERNAME_CHARS = /^@?[a-zA-Z_][a-zA-Z0-9_]{2,31}(.\d+)?$/;
|
||||
const USERNAME_LIKE = /^@?[a-zA-Z_][a-zA-Z0-9_]{2,31}(.\d*?)?$/;
|
||||
const NICKNAME_CHARS = /^[a-zA-Z_][a-zA-Z0-9_]+$/;
|
||||
const ALL_DIGITS = /^\d+$/;
|
||||
|
||||
export function isUsernameValid(username: string): boolean {
|
||||
const nickname = getNickname(username);
|
||||
const discriminator = getDiscriminator(username);
|
||||
|
||||
if (!nickname) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
nickname.length < getMinNickname() ||
|
||||
nickname.length > getMaxNickname()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!NICKNAME_CHARS.test(nickname)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!discriminator || discriminator.length === 0) {
|
||||
return false;
|
||||
}
|
||||
if (discriminator[0] === '0' && discriminator[1] === '0') {
|
||||
return false;
|
||||
}
|
||||
if (discriminator[0] === '0' && discriminator.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getUsernameFromSearch(searchTerm: string): string | undefined {
|
||||
let modifiedTerm = searchTerm.trim();
|
||||
|
||||
@@ -28,26 +62,12 @@ export function getUsernameFromSearch(searchTerm: string): string | undefined {
|
||||
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)) {
|
||||
if (!USERNAME_LIKE.test(modifiedTerm)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return modifiedTerm;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return modifiedTerm;
|
||||
}
|
||||
|
||||
export function isProbablyAUsername(text: string): boolean {
|
||||
@@ -57,7 +77,7 @@ export function isProbablyAUsername(text: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!USERNAME_CHARS.test(searchTerm)) {
|
||||
if (!USERNAME_LIKE.test(searchTerm)) {
|
||||
return false;
|
||||
}
|
||||
if (ALL_DIGITS.test(searchTerm)) {
|
||||
|
||||
Reference in New Issue
Block a user