Allow adding to a group by phone number

This commit is contained in:
Fedor Indutny 2022-04-04 17:38:22 -07:00 committed by GitHub
parent 76a1a805ef
commit 9568d5792e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1842 additions and 693 deletions

View file

@ -1,16 +1,16 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { FuseOptions } from 'fuse.js';
import Fuse from 'fuse.js';
import type { ConversationType } from '../state/ducks/conversations';
import { parseAndFormatPhoneNumber } from './libphonenumberInstance';
const FUSE_OPTIONS: FuseOptions<ConversationType> = {
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationType> = {
// A small-but-nonzero threshold lets us match parts of E164s better, and makes the
// search a little more forgiving.
threshold: 0.05,
tokenize: true,
threshold: 0.1,
useExtendedSearch: true,
keys: [
{
name: 'searchableTitle',
@ -37,21 +37,45 @@ const FUSE_OPTIONS: FuseOptions<ConversationType> = {
const collator = new Intl.Collator();
const cachedIndices = new WeakMap<
ReadonlyArray<ConversationType>,
Fuse<ConversationType>
>();
// See https://fusejs.io/examples.html#extended-search for
// extended search documentation.
function searchConversations(
conversations: ReadonlyArray<ConversationType>,
searchTerm: string
searchTerm: string,
regionCode: string | undefined
): Array<ConversationType> {
return new Fuse<ConversationType>(conversations, FUSE_OPTIONS).search(
searchTerm
);
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
// Escape the search term
let extendedSearchTerm = searchTerm;
// OR phoneNumber
if (phoneNumber) {
extendedSearchTerm += ` | ${phoneNumber.e164}`;
}
let index = cachedIndices.get(conversations);
if (!index) {
index = new Fuse<ConversationType>(conversations, FUSE_OPTIONS);
cachedIndices.set(conversations, index);
}
const results = index.search(extendedSearchTerm);
return results.map(result => result.item);
}
export function filterAndSortConversationsByRecent(
conversations: ReadonlyArray<ConversationType>,
searchTerm: string
searchTerm: string,
regionCode: string | undefined
): Array<ConversationType> {
if (searchTerm.length) {
return searchConversations(conversations, searchTerm);
return searchConversations(conversations, searchTerm, regionCode);
}
return conversations.concat().sort((a, b) => {
@ -65,10 +89,11 @@ export function filterAndSortConversationsByRecent(
export function filterAndSortConversationsByTitle(
conversations: ReadonlyArray<ConversationType>,
searchTerm: string
searchTerm: string,
regionCode: string | undefined
): Array<ConversationType> {
if (searchTerm.length) {
return searchConversations(conversations, searchTerm);
return searchConversations(conversations, searchTerm, regionCode);
}
return conversations.concat().sort((a, b) => {

View file

@ -2,8 +2,34 @@
// SPDX-License-Identifier: AGPL-3.0-only
import libphonenumber from 'google-libphonenumber';
import type { PhoneNumber } from 'google-libphonenumber';
const instance = libphonenumber.PhoneNumberUtil.getInstance();
const { PhoneNumberFormat } = libphonenumber;
export { instance, PhoneNumberFormat };
export type ParsedE164Type = Readonly<{
isValid: boolean;
userInput: string;
e164: string;
}>;
export function parseAndFormatPhoneNumber(
str: string,
regionCode: string | undefined,
format = PhoneNumberFormat.E164
): ParsedE164Type | undefined {
let result: PhoneNumber;
try {
result = instance.parse(str, regionCode);
} catch (err) {
return undefined;
}
return {
isValid: instance.isValidNumber(result),
userInput: str,
e164: instance.format(result, format),
};
}

View file

@ -1636,6 +1636,18 @@
"reasonCategory": "falseMatch",
"updated": "2020-09-11T17:24:56.124Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/fuse.js/dist/fuse.basic.min.js",
"reasonCategory": "falseMatch",
"updated": "2022-03-31T19:50:28.622Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/fuse.js/dist/fuse.min.js",
"reasonCategory": "falseMatch",
"updated": "2022-03-31T19:50:28.622Z"
},
{
"rule": "jQuery-load(",
"path": "node_modules/get-uri/node_modules/debug/src/browser.js",

View file

@ -0,0 +1,159 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { ToastFailedToFetchUsername } from '../components/ToastFailedToFetchUsername';
import { ToastFailedToFetchPhoneNumber } from '../components/ToastFailedToFetchPhoneNumber';
import type { UserNotFoundModalStateType } from '../state/ducks/globalModals';
import * as log from '../logging/log';
import { UUID } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
import { isValidUsername } from '../types/Username';
import * as Errors from '../types/errors';
import { HTTPError } from '../textsecure/Errors';
import { showToast } from './showToast';
import { strictAssert } from './assert';
import type { UUIDFetchStateKeyType } from './uuidFetchState';
export type LookupConversationWithoutUuidActionsType = Readonly<{
lookupConversationWithoutUuid: typeof lookupConversationWithoutUuid;
showUserNotFoundModal: (state: UserNotFoundModalStateType) => void;
setIsFetchingUUID: (
identifier: UUIDFetchStateKeyType,
isFetching: boolean
) => void;
}>;
export type LookupConversationWithoutUuidOptionsType = Omit<
LookupConversationWithoutUuidActionsType,
'lookupConversationWithoutUuid'
> &
Readonly<
| {
type: 'e164';
e164: string;
phoneNumber: string;
}
| {
type: 'username';
username: string;
}
>;
type FoundUsernameType = {
uuid: UUIDStringType;
username: string;
};
export async function lookupConversationWithoutUuid(
options: LookupConversationWithoutUuidOptionsType
): Promise<string | undefined> {
const knownConversation = window.ConversationController.get(
options.type === 'e164' ? options.e164 : options.username
);
if (knownConversation && knownConversation.get('uuid')) {
return knownConversation.id;
}
const identifier: UUIDFetchStateKeyType =
options.type === 'e164'
? `e164:${options.e164}`
: `username:${options.username}`;
const { showUserNotFoundModal, setIsFetchingUUID } = options;
setIsFetchingUUID(identifier, true);
try {
let conversationId: string | undefined;
if (options.type === 'e164') {
const serverLookup = await window.textsecure.messaging.getUuidsForE164s([
options.e164,
]);
if (serverLookup[options.e164]) {
conversationId = window.ConversationController.ensureContactIds({
e164: options.e164,
uuid: serverLookup[options.e164],
highTrust: true,
reason: 'startNewConversationWithoutUuid(e164)',
});
}
} else {
const foundUsername = await checkForUsername(options.username);
if (foundUsername) {
conversationId = window.ConversationController.ensureContactIds({
uuid: foundUsername.uuid,
highTrust: true,
reason: 'startNewConversationWithoutUuid(username)',
});
const convo = window.ConversationController.get(conversationId);
strictAssert(convo, 'We just ensured conversation existence');
convo.set({ username: foundUsername.username });
}
}
if (!conversationId) {
showUserNotFoundModal(
options.type === 'username'
? options
: {
type: 'phoneNumber',
phoneNumber: options.phoneNumber,
}
);
return undefined;
}
return conversationId;
} catch (error) {
log.error(
'startNewConversationWithoutUuid: Something went wrong fetching:',
Errors.toLogFormat(error)
);
if (options.type === 'e164') {
showToast(ToastFailedToFetchPhoneNumber);
} else {
showToast(ToastFailedToFetchUsername);
}
return undefined;
} finally {
setIsFetchingUUID(identifier, false);
}
}
async function checkForUsername(
username: string
): Promise<FoundUsernameType | undefined> {
if (!isValidUsername(username)) {
return undefined;
}
try {
const profile = await window.textsecure.messaging.getProfileForUsername(
username
);
if (!profile.uuid) {
log.error("checkForUsername: Returned profile didn't include a uuid");
return;
}
return {
uuid: UUID.cast(profile.uuid),
username,
};
} catch (error) {
if (!(error instanceof HTTPError)) {
throw error;
}
if (error.code === 404) {
return undefined;
}
throw error;
}
}

19
ts/util/uuidFetchState.ts Normal file
View file

@ -0,0 +1,19 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export type UUIDFetchStateKeyType = `${'username' | 'e164'}:${string}`;
export type UUIDFetchStateType = Record<UUIDFetchStateKeyType, boolean>;
export const isFetchingByUsername = (
fetchState: UUIDFetchStateType,
username: string
): boolean => {
return Boolean(fetchState[`username:${username}`]);
};
export const isFetchingByE164 = (
fetchState: UUIDFetchStateType,
e164: string
): boolean => {
return Boolean(fetchState[`e164:${e164}`]);
};