Allow adding to a group by phone number
This commit is contained in:
parent
76a1a805ef
commit
9568d5792e
49 changed files with 1842 additions and 693 deletions
|
@ -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) => {
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
159
ts/util/lookupConversationWithoutUuid.ts
Normal file
159
ts/util/lookupConversationWithoutUuid.ts
Normal 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
19
ts/util/uuidFetchState.ts
Normal 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}`]);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue