= React.memo(
- function StartNewConversation({ i18n, onClick, phoneNumber }) {
- const messageText = (
- {i18n('startConversation')}
- );
+ function StartNewConversation({
+ i18n,
+ phoneNumber,
+ isFetching,
+ lookupConversationWithoutUuid,
+ showUserNotFoundModal,
+ setIsFetchingUUID,
+ showConversation,
+ }) {
+ const [isModalVisible, setIsModalVisible] = useState(false);
- const boundOnClick = useCallback(() => {
- onClick(phoneNumber);
- }, [onClick, phoneNumber]);
+ const boundOnClick = useCallback(async () => {
+ if (!phoneNumber.isValid) {
+ setIsModalVisible(true);
+ return;
+ }
+ if (isFetching) {
+ return;
+ }
+ const conversationId = await lookupConversationWithoutUuid({
+ showUserNotFoundModal,
+ setIsFetchingUUID,
+
+ type: 'e164',
+ e164: phoneNumber.e164,
+ phoneNumber: phoneNumber.userInput,
+ });
+
+ if (conversationId !== undefined) {
+ showConversation(conversationId);
+ }
+ }, [
+ showConversation,
+ lookupConversationWithoutUuid,
+ showUserNotFoundModal,
+ setIsFetchingUUID,
+ setIsModalVisible,
+ phoneNumber,
+ isFetching,
+ ]);
+
+ let modal: JSX.Element | undefined;
+ if (isModalVisible) {
+ modal = (
+ setIsModalVisible(false)}
+ >
+ {i18n('startConversation--phone-number-not-valid', {
+ phoneNumber: phoneNumber.userInput,
+ })}
+
+ );
+ }
return (
-
+ <>
+
+ {modal}
+ >
);
}
);
diff --git a/ts/components/conversationList/UsernameSearchResultListItem.tsx b/ts/components/conversationList/UsernameSearchResultListItem.tsx
index ebbacff65d4..645cfb62366 100644
--- a/ts/components/conversationList/UsernameSearchResultListItem.tsx
+++ b/ts/components/conversationList/UsernameSearchResultListItem.tsx
@@ -2,12 +2,13 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { FunctionComponent } from 'react';
-import React from 'react';
-import { noop } from 'lodash';
+import React, { useCallback } from 'react';
import { BaseConversationListItem } from './BaseConversationListItem';
import type { LocalizerType } from '../../types/Util';
+import { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid';
+import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
type PropsData = {
username: string;
@@ -16,23 +17,42 @@ type PropsData = {
type PropsHousekeeping = {
i18n: LocalizerType;
- onClick: (username: string) => void;
-};
+ showConversation: (conversationId: string) => void;
+} & LookupConversationWithoutUuidActionsType;
export type Props = PropsData & PropsHousekeeping;
export const UsernameSearchResultListItem: FunctionComponent = ({
i18n,
isFetchingUsername,
- onClick,
username,
+ showUserNotFoundModal,
+ setIsFetchingUUID,
+ showConversation,
}) => {
const usernameText = i18n('at-username', { username });
- const boundOnClick = isFetchingUsername
- ? noop
- : () => {
- onClick(username);
- };
+ const boundOnClick = useCallback(async () => {
+ if (isFetchingUsername) {
+ return;
+ }
+ const conversationId = await lookupConversationWithoutUuid({
+ showUserNotFoundModal,
+ setIsFetchingUUID,
+
+ type: 'username',
+ username,
+ });
+
+ if (conversationId !== undefined) {
+ showConversation(conversationId);
+ }
+ }, [
+ username,
+ showUserNotFoundModal,
+ setIsFetchingUUID,
+ showConversation,
+ isFetchingUsername,
+ ]);
return (
{
- const results = fuse.search(query.substr(0, 32));
+ const results = fuse.search(query.substr(0, 32)).map(result => result.item);
if (count) {
return take(results, count);
diff --git a/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx b/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx
index ca9fa016e1a..4dd4b9ee29c 100644
--- a/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx
+++ b/ts/components/leftPane/LeftPaneChooseGroupMembersHelper.tsx
@@ -18,46 +18,78 @@ import {
} from '../AddGroupMemberErrorDialog';
import { Button } from '../Button';
import type { LocalizerType } from '../../types/Util';
+import type { ParsedE164Type } from '../../util/libphonenumberInstance';
+import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
+import type { UUIDFetchStateType } from '../../util/uuidFetchState';
+import { isFetchingByE164 } from '../../util/uuidFetchState';
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
} from '../../groups/limits';
export type LeftPaneChooseGroupMembersPropsType = {
+ uuidFetchState: UUIDFetchStateType;
candidateContacts: ReadonlyArray;
isShowingRecommendedGroupSizeModal: boolean;
isShowingMaximumGroupSizeModal: boolean;
searchTerm: string;
+ regionCode: string | undefined;
selectedContacts: Array;
};
export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper {
private readonly candidateContacts: ReadonlyArray;
+ private readonly isPhoneNumberChecked: boolean;
+
private readonly isShowingMaximumGroupSizeModal: boolean;
private readonly isShowingRecommendedGroupSizeModal: boolean;
private readonly searchTerm: string;
+ private readonly phoneNumber: ParsedE164Type | undefined;
+
private readonly selectedContacts: Array;
private readonly selectedConversationIdsSet: Set;
+ private readonly uuidFetchState: UUIDFetchStateType;
+
constructor({
candidateContacts,
isShowingMaximumGroupSizeModal,
isShowingRecommendedGroupSizeModal,
searchTerm,
+ regionCode,
selectedContacts,
+ uuidFetchState,
}: Readonly) {
super();
+ this.uuidFetchState = uuidFetchState;
+
this.candidateContacts = candidateContacts;
this.isShowingMaximumGroupSizeModal = isShowingMaximumGroupSizeModal;
this.isShowingRecommendedGroupSizeModal =
isShowingRecommendedGroupSizeModal;
this.searchTerm = searchTerm;
+
+ const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
+ if (phoneNumber) {
+ this.isPhoneNumberChecked =
+ phoneNumber.isValid &&
+ selectedContacts.some(contact => contact.e164 === phoneNumber.e164);
+
+ const isVisible = this.candidateContacts.every(
+ contact => contact.e164 !== phoneNumber.e164
+ );
+ if (isVisible) {
+ this.phoneNumber = phoneNumber;
+ }
+ } else {
+ this.isPhoneNumberChecked = false;
+ }
this.selectedContacts = selectedContacts;
this.selectedConversationIdsSet = new Set(
@@ -207,46 +239,90 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper 0) {
+ rowCount += 1;
+ }
+
+ return rowCount;
}
- getRow(rowIndex: number): undefined | Row {
- if (!this.candidateContacts.length) {
+ getRow(actualRowIndex: number): undefined | Row {
+ if (!this.candidateContacts.length && !this.phoneNumber) {
return undefined;
}
- if (rowIndex === 0) {
- return {
- type: RowType.Header,
- i18nKey: 'contactsHeader',
- };
- }
+ const rowCount = this.getRowCount();
// This puts a blank row for the footer.
- if (rowIndex === this.candidateContacts.length + 1) {
+ if (actualRowIndex === rowCount - 1) {
return { type: RowType.Blank };
}
- const contact = this.candidateContacts[rowIndex - 1];
- if (!contact) {
- return undefined;
+ let virtualRowIndex = actualRowIndex;
+
+ if (this.candidateContacts.length) {
+ if (virtualRowIndex === 0) {
+ return {
+ type: RowType.Header,
+ i18nKey: 'contactsHeader',
+ };
+ }
+
+ if (virtualRowIndex <= this.candidateContacts.length) {
+ const contact = this.candidateContacts[virtualRowIndex - 1];
+
+ const isChecked = this.selectedConversationIdsSet.has(contact.id);
+ const disabledReason =
+ !isChecked && this.hasSelectedMaximumNumberOfContacts()
+ ? ContactCheckboxDisabledReason.MaximumContactsSelected
+ : undefined;
+
+ return {
+ type: RowType.ContactCheckbox,
+ contact,
+ isChecked,
+ disabledReason,
+ };
+ }
+
+ virtualRowIndex -= 1 + this.candidateContacts.length;
}
- const isChecked = this.selectedConversationIdsSet.has(contact.id);
- const disabledReason =
- !isChecked && this.hasSelectedMaximumNumberOfContacts()
- ? ContactCheckboxDisabledReason.MaximumContactsSelected
- : undefined;
+ if (this.phoneNumber) {
+ if (virtualRowIndex === 0) {
+ return {
+ type: RowType.Header,
+ i18nKey: 'findByPhoneNumberHeader',
+ };
+ }
+ if (virtualRowIndex === 1) {
+ return {
+ type: RowType.PhoneNumberCheckbox,
+ isChecked: this.isPhoneNumberChecked,
+ isFetching: isFetchingByE164(
+ this.uuidFetchState,
+ this.phoneNumber.e164
+ ),
+ phoneNumber: this.phoneNumber,
+ };
+ }
+ virtualRowIndex -= 2;
+ }
- return {
- type: RowType.ContactCheckbox,
- contact,
- isChecked,
- disabledReason,
- };
+ return undefined;
}
// This is deliberately unimplemented because these keyboard shortcuts shouldn't work in
diff --git a/ts/components/leftPane/LeftPaneComposeHelper.tsx b/ts/components/leftPane/LeftPaneComposeHelper.tsx
index c7ee99f0829..49f47a103bb 100644
--- a/ts/components/leftPane/LeftPaneComposeHelper.tsx
+++ b/ts/components/leftPane/LeftPaneComposeHelper.tsx
@@ -3,7 +3,6 @@
import type { ReactChild, ChangeEvent } from 'react';
import React from 'react';
-import type { PhoneNumber } from 'google-libphonenumber';
import { LeftPaneHelper } from './LeftPaneHelper';
import type { Row } from '../ConversationList';
@@ -12,13 +11,15 @@ import type { ContactListItemConversationType } from '../conversationList/Contac
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
import { SearchInput } from '../SearchInput';
import type { LocalizerType } from '../../types/Util';
-import {
- instance as phoneNumberInstance,
- PhoneNumberFormat,
-} from '../../util/libphonenumberInstance';
-import { assert } from '../../util/assert';
+import type { ParsedE164Type } from '../../util/libphonenumberInstance';
+import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
import { missingCaseError } from '../../util/missingCaseError';
import { getUsernameFromSearch } from '../../types/Username';
+import type { UUIDFetchStateType } from '../../util/uuidFetchState';
+import {
+ isFetchingByUsername,
+ isFetchingByE164,
+} from '../../util/uuidFetchState';
export type LeftPaneComposePropsType = {
composeContacts: ReadonlyArray;
@@ -26,14 +27,13 @@ export type LeftPaneComposePropsType = {
regionCode: string | undefined;
searchTerm: string;
- isFetchingUsername: boolean;
+ uuidFetchState: UUIDFetchStateType;
isUsernamesEnabled: boolean;
};
enum TopButton {
None,
CreateNewGroup,
- StartNewConversation,
}
export class LeftPaneComposeHelper extends LeftPaneHelper {
@@ -41,13 +41,15 @@ export class LeftPaneComposeHelper extends LeftPaneHelper;
- private readonly isFetchingUsername: boolean;
+ private readonly uuidFetchState: UUIDFetchStateType;
private readonly isUsernamesEnabled: boolean;
private readonly searchTerm: string;
- private readonly phoneNumber: undefined | PhoneNumber;
+ private readonly phoneNumber: ParsedE164Type | undefined;
+
+ private readonly isPhoneNumberVisible: boolean;
constructor({
composeContacts,
@@ -55,15 +57,23 @@ export class LeftPaneComposeHelper extends LeftPaneHelper) {
super();
this.composeContacts = composeContacts;
this.composeGroups = composeGroups;
this.searchTerm = searchTerm;
- this.phoneNumber = parsePhoneNumber(searchTerm, regionCode);
- this.isFetchingUsername = isFetchingUsername;
+ this.phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
+ if (this.phoneNumber) {
+ const { phoneNumber } = this;
+ this.isPhoneNumberVisible = this.composeContacts.every(
+ contact => contact.e164 !== phoneNumber.e164
+ );
+ } else {
+ this.isPhoneNumberVisible = false;
+ }
+ this.uuidFetchState = uuidFetchState;
this.isUsernamesEnabled = isUsernamesEnabled;
}
@@ -141,6 +151,9 @@ export class LeftPaneComposeHelper extends LeftPaneHelper,
- path: string
+ path: string | Array
): ReadonlyArray | string {
// It'd be nice to avoid this cast, but Fuse's types don't allow it.
- const rawValue = getOwn(conversation as Record, path);
+ const rawValue = get(conversation as Record, path);
if (typeof rawValue !== 'string') {
// It might make more sense to return `undefined` here, but [Fuse's types don't
@@ -78,7 +78,7 @@ export class MemberRepository {
this.isFuseReady = true;
}
- const results = this.fuse.search(`${pattern}`);
+ const results = this.fuse.search(pattern).map(result => result.item);
if (omit) {
return results.filter(({ id }) => id !== omit.id);
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 0b5bea9c0b8..a8e4cd86a2e 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -21,15 +21,13 @@ import { getOwn } from '../../util/getOwn';
import { assert, strictAssert } from '../../util/assert';
import * as universalExpireTimer from '../../util/universalExpireTimer';
import { trigger } from '../../shims/events';
-import type {
- ShowUsernameNotFoundModalActionType,
- ToggleProfileEditorErrorActionType,
-} from './globalModals';
-import {
- TOGGLE_PROFILE_EDITOR_ERROR,
- actions as globalModalActions,
-} from './globalModals';
+import type { ToggleProfileEditorErrorActionType } from './globalModals';
+import { TOGGLE_PROFILE_EDITOR_ERROR } from './globalModals';
import { isRecord } from '../../util/isRecord';
+import type {
+ UUIDFetchStateKeyType,
+ UUIDFetchStateType,
+} from '../../util/uuidFetchState';
import type {
AvatarColorType,
@@ -45,7 +43,6 @@ import type { BodyRangeType } from '../../types/Util';
import { CallMode } from '../../types/Calling';
import type { MediaItemType } from '../../types/MediaItem';
import type { UUIDStringType } from '../../types/UUID';
-import { UUID } from '../../types/UUID';
import {
getGroupSizeRecommendedLimit,
getGroupSizeHardLimit,
@@ -57,7 +54,6 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
import { writeProfile } from '../../services/writeProfile';
import { writeUsername } from '../../services/writeUsername';
import {
- getConversationsByUsername,
getConversationIdsStoppingSend,
getConversationIdsStoppedForVerification,
getMe,
@@ -76,8 +72,6 @@ import {
} from './conversationsEnums';
import { showToast } from '../../util/showToast';
import { ToastFailedToDeleteUsername } from '../../components/ToastFailedToDeleteUsername';
-import { ToastFailedToFetchUsername } from '../../components/ToastFailedToFetchUsername';
-import { isValidUsername } from '../../types/Username';
import { useBoundActions } from '../../hooks/useBoundActions';
import type { NoopActionType } from './noop';
@@ -288,20 +282,16 @@ export type ConversationVerificationData =
canceledAt: number;
};
-export type FoundUsernameType = {
- uuid: UUIDStringType;
- username: string;
-};
-
type ComposerStateType =
| {
step: ComposerStep.StartDirectConversation;
searchTerm: string;
- isFetchingUsername: boolean;
+ uuidFetchState: UUIDFetchStateType;
}
| ({
step: ComposerStep.ChooseGroupMembers;
searchTerm: string;
+ uuidFetchState: UUIDFetchStateType;
} & ComposerGroupCreationState)
| ({
step: ComposerStep.SetGroupMetadata;
@@ -677,10 +667,11 @@ type SetComposeSearchTermActionType = {
type: 'SET_COMPOSE_SEARCH_TERM';
payload: { searchTerm: string };
};
-type SetIsFetchingUsernameActionType = {
- type: 'SET_IS_FETCHING_USERNAME';
+type SetIsFetchingUUIDActionType = {
+ type: 'SET_IS_FETCHING_UUID';
payload: {
- isFetchingUsername: boolean;
+ identifier: UUIDFetchStateKeyType;
+ isFetching: boolean;
};
};
type SetRecentMediaItemsActionType = {
@@ -773,7 +764,7 @@ export type ConversationActionType =
| SetComposeGroupNameActionType
| SetComposeSearchTermActionType
| SetConversationHeaderTitleActionType
- | SetIsFetchingUsernameActionType
+ | SetIsFetchingUUIDActionType
| SetIsNearBottomActionType
| SetMessageLoadingStateActionType
| SetPreJoinConversationActionType
@@ -840,6 +831,7 @@ export const actions = {
setComposeGroupExpireTimer,
setComposeGroupName,
setComposeSearchTerm,
+ setIsFetchingUUID,
setIsNearBottom,
setMessageLoadingState,
setPreJoinConversation,
@@ -849,9 +841,8 @@ export const actions = {
showArchivedConversations,
showChooseGroupMembers,
showInbox,
+ showConversation,
startComposing,
- startNewConversationFromPhoneNumber,
- startNewConversationFromUsername,
startSettingGroupMetadata,
toggleAdmin,
toggleConversationInChooseMembers,
@@ -1661,6 +1652,18 @@ function setIsNearBottom(
},
};
}
+function setIsFetchingUUID(
+ identifier: UUIDFetchStateKeyType,
+ isFetching: boolean
+): SetIsFetchingUUIDActionType {
+ return {
+ type: 'SET_IS_FETCHING_UUID',
+ payload: {
+ identifier,
+ isFetching,
+ },
+ };
+}
function setSelectedConversationHeaderTitle(
title?: string
): SetConversationHeaderTitleActionType {
@@ -1772,117 +1775,6 @@ function showChooseGroupMembers(): ShowChooseGroupMembersActionType {
return { type: 'SHOW_CHOOSE_GROUP_MEMBERS' };
}
-function startNewConversationFromPhoneNumber(
- e164: string
-): ThunkAction {
- return dispatch => {
- trigger('showConversation', e164);
-
- dispatch(showInbox());
- };
-}
-
-async function checkForUsername(
- username: string
-): Promise {
- 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: unknown) {
- if (!isRecord(error)) {
- throw error;
- }
-
- if (error.code === 404) {
- return undefined;
- }
-
- throw error;
- }
-}
-
-function startNewConversationFromUsername(
- username: string
-): ThunkAction<
- void,
- RootStateType,
- unknown,
- | ShowInboxActionType
- | SetIsFetchingUsernameActionType
- | ShowUsernameNotFoundModalActionType
-> {
- return async (dispatch, getState) => {
- const state = getState();
-
- const byUsername = getConversationsByUsername(state);
- const knownConversation = getOwn(byUsername, username);
- if (knownConversation && knownConversation.uuid) {
- trigger('showConversation', knownConversation.uuid, username);
- dispatch(showInbox());
- return;
- }
-
- dispatch({
- type: 'SET_IS_FETCHING_USERNAME',
- payload: {
- isFetchingUsername: true,
- },
- });
-
- try {
- const foundUsername = await checkForUsername(username);
- dispatch({
- type: 'SET_IS_FETCHING_USERNAME',
- payload: {
- isFetchingUsername: false,
- },
- });
-
- if (!foundUsername) {
- dispatch(globalModalActions.showUsernameNotFoundModal(username));
- return;
- }
-
- trigger(
- 'showConversation',
- foundUsername.uuid,
- undefined,
- foundUsername.username
- );
- dispatch(showInbox());
- } catch (error) {
- log.error(
- 'startNewConversationFromUsername: Something went wrong fetching username:',
- error.stack
- );
-
- dispatch({
- type: 'SET_IS_FETCHING_USERNAME',
- payload: {
- isFetchingUsername: false,
- },
- });
-
- showToast(ToastFailedToFetchUsername);
- }
- };
-}
-
function startSettingGroupMetadata(): StartSettingGroupMetadataActionType {
return { type: 'START_SETTING_GROUP_METADATA' };
}
@@ -2029,6 +1921,14 @@ function showInbox(): ShowInboxActionType {
payload: null,
};
}
+function showConversation(
+ conversationId: string
+): ThunkAction {
+ return dispatch => {
+ trigger('showConversation', conversationId);
+ dispatch(showInbox());
+ };
+}
function showArchivedConversations(): ShowArchivedConversationsActionType {
return {
type: 'SHOW_ARCHIVED_CONVERSATIONS',
@@ -3060,7 +2960,7 @@ export function reducer(
composer: {
step: ComposerStep.StartDirectConversation,
searchTerm: '',
- isFetchingUsername: false,
+ uuidFetchState: {},
},
};
}
@@ -3103,6 +3003,7 @@ export function reducer(
composer: {
step: ComposerStep.ChooseGroupMembers,
searchTerm: '',
+ uuidFetchState: {},
selectedConversationIds,
recommendedGroupSizeModalState,
maximumGroupSizeModalState,
@@ -3235,26 +3136,36 @@ export function reducer(
};
}
- if (action.type === 'SET_IS_FETCHING_USERNAME') {
+ if (action.type === 'SET_IS_FETCHING_UUID') {
const { composer } = state;
if (!composer) {
assert(
false,
- 'Setting compose username with the composer closed is a no-op'
+ 'Setting compose uuid fetch state with the composer closed is a no-op'
);
return state;
}
- if (composer.step !== ComposerStep.StartDirectConversation) {
- assert(false, 'Setting compose username at this step is a no-op');
+ if (
+ composer.step !== ComposerStep.StartDirectConversation &&
+ composer.step !== ComposerStep.ChooseGroupMembers
+ ) {
+ assert(false, 'Setting compose uuid fetch state at this step is a no-op');
return state;
}
- const { isFetchingUsername } = action.payload;
+ const { identifier, isFetching } = action.payload;
+
+ const { uuidFetchState } = composer;
return {
...state,
composer: {
...composer,
- isFetchingUsername,
+ uuidFetchState: isFetching
+ ? {
+ ...composer.uuidFetchState,
+ [identifier]: isFetching,
+ }
+ : omit(uuidFetchState, identifier),
},
};
}
diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts
index 0bf3bcd7fd1..bedefce1ce5 100644
--- a/ts/state/ducks/globalModals.ts
+++ b/ts/state/ducks/globalModals.ts
@@ -9,7 +9,7 @@ export type GlobalModalsStateType = {
readonly isWhatsNewVisible: boolean;
readonly profileEditorHasError: boolean;
readonly safetyNumberModalContactId?: string;
- readonly usernameNotFoundModalState?: UsernameNotFoundModalStateType;
+ readonly userNotFoundModalState?: UserNotFoundModalStateType;
};
// Actions
@@ -17,10 +17,8 @@ export type GlobalModalsStateType = {
const HIDE_CONTACT_MODAL = 'globalModals/HIDE_CONTACT_MODAL';
const SHOW_CONTACT_MODAL = 'globalModals/SHOW_CONTACT_MODAL';
const SHOW_WHATS_NEW_MODAL = 'globalModals/SHOW_WHATS_NEW_MODAL_MODAL';
-const SHOW_USERNAME_NOT_FOUND_MODAL =
- 'globalModals/SHOW_USERNAME_NOT_FOUND_MODAL';
-const HIDE_USERNAME_NOT_FOUND_MODAL =
- 'globalModals/HIDE_USERNAME_NOT_FOUND_MODAL';
+const SHOW_UUID_NOT_FOUND_MODAL = 'globalModals/SHOW_UUID_NOT_FOUND_MODAL';
+const HIDE_UUID_NOT_FOUND_MODAL = 'globalModals/HIDE_UUID_NOT_FOUND_MODAL';
const HIDE_WHATS_NEW_MODAL = 'globalModals/HIDE_WHATS_NEW_MODAL_MODAL';
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
export const TOGGLE_PROFILE_EDITOR_ERROR =
@@ -32,9 +30,15 @@ export type ContactModalStateType = {
conversationId?: string;
};
-export type UsernameNotFoundModalStateType = {
- username: string;
-};
+export type UserNotFoundModalStateType =
+ | {
+ type: 'phoneNumber';
+ phoneNumber: string;
+ }
+ | {
+ type: 'username';
+ username: string;
+ };
type HideContactModalActionType = {
type: typeof HIDE_CONTACT_MODAL;
@@ -53,15 +57,13 @@ type ShowWhatsNewModalActionType = {
type: typeof SHOW_WHATS_NEW_MODAL;
};
-type HideUsernameNotFoundModalActionType = {
- type: typeof HIDE_USERNAME_NOT_FOUND_MODAL;
+type HideUserNotFoundModalActionType = {
+ type: typeof HIDE_UUID_NOT_FOUND_MODAL;
};
-export type ShowUsernameNotFoundModalActionType = {
- type: typeof SHOW_USERNAME_NOT_FOUND_MODAL;
- payload: {
- username: string;
- };
+export type ShowUserNotFoundModalActionType = {
+ type: typeof SHOW_UUID_NOT_FOUND_MODAL;
+ payload: UserNotFoundModalStateType;
};
type ToggleProfileEditorActionType = {
@@ -82,8 +84,8 @@ export type GlobalModalsActionType =
| ShowContactModalActionType
| HideWhatsNewModalActionType
| ShowWhatsNewModalActionType
- | HideUsernameNotFoundModalActionType
- | ShowUsernameNotFoundModalActionType
+ | HideUserNotFoundModalActionType
+ | ShowUserNotFoundModalActionType
| ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType
| ToggleSafetyNumberModalActionType;
@@ -95,8 +97,8 @@ export const actions = {
showContactModal,
hideWhatsNewModal,
showWhatsNewModal,
- hideUsernameNotFoundModal,
- showUsernameNotFoundModal,
+ hideUserNotFoundModal,
+ showUserNotFoundModal,
toggleProfileEditor,
toggleProfileEditorHasError,
toggleSafetyNumberModal,
@@ -133,20 +135,18 @@ function showWhatsNewModal(): ShowWhatsNewModalActionType {
};
}
-function hideUsernameNotFoundModal(): HideUsernameNotFoundModalActionType {
+function hideUserNotFoundModal(): HideUserNotFoundModalActionType {
return {
- type: HIDE_USERNAME_NOT_FOUND_MODAL,
+ type: HIDE_UUID_NOT_FOUND_MODAL,
};
}
-function showUsernameNotFoundModal(
- username: string
-): ShowUsernameNotFoundModalActionType {
+function showUserNotFoundModal(
+ payload: UserNotFoundModalStateType
+): ShowUserNotFoundModalActionType {
return {
- type: SHOW_USERNAME_NOT_FOUND_MODAL,
- payload: {
- username,
- },
+ type: SHOW_UUID_NOT_FOUND_MODAL,
+ payload,
};
}
@@ -209,20 +209,18 @@ export function reducer(
};
}
- if (action.type === HIDE_USERNAME_NOT_FOUND_MODAL) {
+ if (action.type === HIDE_UUID_NOT_FOUND_MODAL) {
return {
...state,
- usernameNotFoundModalState: undefined,
+ userNotFoundModalState: undefined,
};
}
- if (action.type === SHOW_USERNAME_NOT_FOUND_MODAL) {
- const { username } = action.payload;
-
+ if (action.type === SHOW_UUID_NOT_FOUND_MODAL) {
return {
...state,
- usernameNotFoundModalState: {
- username,
+ userNotFoundModalState: {
+ ...action.payload,
},
};
}
diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts
index 384a41272f0..ea23ef73be8 100644
--- a/ts/state/selectors/conversations.ts
+++ b/ts/state/selectors/conversations.ts
@@ -25,6 +25,7 @@ import {
} from '../ducks/conversationsEnums';
import { getOwn } from '../../util/getOwn';
import { isNotNil } from '../../util/isNotNil';
+import type { UUIDFetchStateType } from '../../util/uuidFetchState';
import { deconstructLookup } from '../../util/deconstructLookup';
import type { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
@@ -394,21 +395,25 @@ export const getComposerConversationSearchTerm = createSelector(
}
);
-export const getIsFetchingUsername = createSelector(
+export const getComposerUUIDFetchState = createSelector(
getComposerState,
- (composer): boolean => {
+ (composer): UUIDFetchStateType => {
if (!composer) {
assert(false, 'getIsFetchingUsername: composer is not open');
- return false;
+ return {};
}
- if (composer.step !== ComposerStep.StartDirectConversation) {
+ if (
+ composer.step !== ComposerStep.StartDirectConversation &&
+ composer.step !== ComposerStep.ChooseGroupMembers
+ ) {
assert(
false,
- `getIsFetchingUsername: step ${composer.step} has no isFetchingUsername key`
+ `getComposerUUIDFetchState: step ${composer.step} ` +
+ 'has no uuidFetchState key'
);
- return false;
+ return {};
}
- return composer.isFetchingUsername;
+ return composer.uuidFetchState;
}
);
@@ -512,28 +517,33 @@ const getNormalizedComposerConversationSearchTerm = createSelector(
export const getFilteredComposeContacts = createSelector(
getNormalizedComposerConversationSearchTerm,
getComposableContacts,
+ getRegionCode,
(
searchTerm: string,
- contacts: Array
+ contacts: Array,
+ regionCode: string | undefined
): Array => {
- return filterAndSortConversationsByTitle(contacts, searchTerm);
+ return filterAndSortConversationsByTitle(contacts, searchTerm, regionCode);
}
);
export const getFilteredComposeGroups = createSelector(
getNormalizedComposerConversationSearchTerm,
getComposableGroups,
+ getRegionCode,
(
searchTerm: string,
- groups: Array
+ groups: Array,
+ regionCode: string | undefined
): Array => {
- return filterAndSortConversationsByTitle(groups, searchTerm);
+ return filterAndSortConversationsByTitle(groups, searchTerm, regionCode);
}
);
export const getFilteredCandidateContactsForNewGroup = createSelector(
getCandidateContactsForNewGroup,
getNormalizedComposerConversationSearchTerm,
+ getRegionCode,
filterAndSortConversationsByTitle
);
diff --git a/ts/state/smart/ChooseGroupMembersModal.tsx b/ts/state/smart/ChooseGroupMembersModal.tsx
new file mode 100644
index 00000000000..765e2a0aeff
--- /dev/null
+++ b/ts/state/smart/ChooseGroupMembersModal.tsx
@@ -0,0 +1,63 @@
+// Copyright 2022 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { connect } from 'react-redux';
+
+import type { StateType } from '../reducer';
+import { mapDispatchToProps } from '../actions';
+import { strictAssert } from '../../util/assert';
+import { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid';
+
+import type { StatePropsType } from '../../components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal';
+import { ChooseGroupMembersModal } from '../../components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal';
+
+import { getIntl, getTheme, getRegionCode } from '../selectors/user';
+import {
+ getCandidateContactsForNewGroup,
+ getConversationByIdSelector,
+} from '../selectors/conversations';
+import { getPreferredBadgeSelector } from '../selectors/badges';
+
+export type SmartChooseGroupMembersModalPropsType = {
+ conversationIdsAlreadyInGroup: Set;
+ maxGroupSize: number;
+ confirmAdds: () => void;
+ onClose: () => void;
+ removeSelectedContact: (_: string) => void;
+ searchTerm: string;
+ selectedConversationIds: ReadonlyArray;
+ setSearchTerm: (_: string) => void;
+ toggleSelectedContact: (conversationId: string) => void;
+};
+
+const mapStateToProps = (
+ state: StateType,
+ props: SmartChooseGroupMembersModalPropsType
+): StatePropsType => {
+ const conversationSelector = getConversationByIdSelector(state);
+
+ const candidateContacts = getCandidateContactsForNewGroup(state);
+ const selectedContacts = props.selectedConversationIds.map(conversationId => {
+ const convo = conversationSelector(conversationId);
+ strictAssert(
+ convo,
+ ' selected conversation not found'
+ );
+ return convo;
+ });
+
+ return {
+ ...props,
+ regionCode: getRegionCode(state),
+ candidateContacts,
+ getPreferredBadge: getPreferredBadgeSelector(state),
+ i18n: getIntl(state),
+ theme: getTheme(state),
+ selectedContacts,
+ lookupConversationWithoutUuid,
+ };
+};
+
+const smart = connect(mapStateToProps, mapDispatchToProps);
+
+export const SmartChooseGroupMembersModal = smart(ChooseGroupMembersModal);
diff --git a/ts/state/smart/ConfirmAdditionsModal.tsx b/ts/state/smart/ConfirmAdditionsModal.tsx
new file mode 100644
index 00000000000..846985f8bb4
--- /dev/null
+++ b/ts/state/smart/ConfirmAdditionsModal.tsx
@@ -0,0 +1,49 @@
+// Copyright 2022 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { connect } from 'react-redux';
+
+import type { StateType } from '../reducer';
+import { mapDispatchToProps } from '../actions';
+import { strictAssert } from '../../util/assert';
+
+import type { StatePropsType } from '../../components/conversation/conversation-details/AddGroupMembersModal/ConfirmAdditionsModal';
+import { ConfirmAdditionsModal } from '../../components/conversation/conversation-details/AddGroupMembersModal/ConfirmAdditionsModal';
+import type { RequestState } from '../../components/conversation/conversation-details/util';
+
+import { getIntl } from '../selectors/user';
+import { getConversationByIdSelector } from '../selectors/conversations';
+
+export type SmartConfirmAdditionsModalPropsType = {
+ selectedConversationIds: ReadonlyArray;
+ groupTitle: string;
+ makeRequest: () => void;
+ onClose: () => void;
+ requestState: RequestState;
+};
+
+const mapStateToProps = (
+ state: StateType,
+ props: SmartConfirmAdditionsModalPropsType
+): StatePropsType => {
+ const conversationSelector = getConversationByIdSelector(state);
+
+ const selectedContacts = props.selectedConversationIds.map(conversationId => {
+ const convo = conversationSelector(conversationId);
+ strictAssert(
+ convo,
+ ' selected conversation not found'
+ );
+ return convo;
+ });
+
+ return {
+ ...props,
+ selectedContacts,
+ i18n: getIntl(state),
+ };
+};
+
+const smart = connect(mapStateToProps, mapDispatchToProps);
+
+export const SmartConfirmAdditionsModal = smart(ConfirmAdditionsModal);
diff --git a/ts/state/smart/ConversationDetails.tsx b/ts/state/smart/ConversationDetails.tsx
index 3150160d193..80b3cfdbec1 100644
--- a/ts/state/smart/ConversationDetails.tsx
+++ b/ts/state/smart/ConversationDetails.tsx
@@ -1,6 +1,7 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
+import React from 'react';
import { connect } from 'react-redux';
import type { StateType } from '../reducer';
@@ -8,7 +9,6 @@ import { mapDispatchToProps } from '../actions';
import type { StateProps } from '../../components/conversation/conversation-details/ConversationDetails';
import { ConversationDetails } from '../../components/conversation/conversation-details/ConversationDetails';
import {
- getCandidateContactsForNewGroup,
getConversationByIdSelector,
getConversationByUuidSelector,
} from '../selectors/conversations';
@@ -24,6 +24,10 @@ import {
import { assert } from '../../util/assert';
import { SignalService as Proto } from '../../protobuf';
import { getConversationColorAttributes } from '../../util/getConversationColorAttributes';
+import type { SmartChooseGroupMembersModalPropsType } from './ChooseGroupMembersModal';
+import { SmartChooseGroupMembersModal } from './ChooseGroupMembersModal';
+import type { SmartConfirmAdditionsModalPropsType } from './ConfirmAdditionsModal';
+import { SmartConfirmAdditionsModal } from './ConfirmAdditionsModal';
export type SmartConversationDetailsProps = {
addMembers: (conversationIds: ReadonlyArray) => Promise;
@@ -56,6 +60,18 @@ export type SmartConversationDetailsProps = {
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
+const renderChooseGroupMembersModal = (
+ props: SmartChooseGroupMembersModalPropsType
+) => {
+ return ;
+};
+
+const renderConfirmAdditionsModal = (
+ props: SmartConfirmAdditionsModalPropsType
+) => {
+ return ;
+};
+
const mapStateToProps = (
state: StateType,
props: SmartConversationDetailsProps
@@ -69,7 +85,6 @@ const mapStateToProps = (
const canEditGroupInfo = Boolean(conversation.canEditGroupInfo);
const isAdmin = Boolean(conversation.areWeAdmin);
- const candidateContactsToAdd = getCandidateContactsForNewGroup(state);
const hasGroupLink =
Boolean(conversation.groupLink) &&
@@ -88,7 +103,6 @@ const mapStateToProps = (
areWeASubscriber: getAreWeASubscriber(state),
badges,
canEditGroupInfo,
- candidateContactsToAdd,
conversation: {
...conversation,
...getConversationColorAttributes(conversation),
@@ -102,6 +116,8 @@ const mapStateToProps = (
hasGroupLink,
isGroup: conversation.type === 'group',
theme: getTheme(state),
+ renderChooseGroupMembersModal,
+ renderConfirmAdditionsModal,
};
};
diff --git a/ts/state/smart/ForwardMessageModal.tsx b/ts/state/smart/ForwardMessageModal.tsx
index 62558b5208f..de2fd2abb57 100644
--- a/ts/state/smart/ForwardMessageModal.tsx
+++ b/ts/state/smart/ForwardMessageModal.tsx
@@ -11,7 +11,7 @@ import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import { getPreferredBadgeSelector } from '../selectors/badges';
import { getAllComposableConversations } from '../selectors/conversations';
import { getLinkPreview } from '../selectors/linkPreviews';
-import { getIntl, getTheme } from '../selectors/user';
+import { getIntl, getTheme, getRegionCode } from '../selectors/user';
import { getEmojiSkinTone } from '../selectors/items';
import { selectRecentEmojis } from '../selectors/emojis';
import type { AttachmentType } from '../../types/Attachment';
@@ -69,6 +69,7 @@ const mapStateToProps = (
skinTone,
onTextTooLong,
theme: getTheme(state),
+ regionCode: getRegionCode(state),
};
};
diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx
index b84b55ebdf9..1e4c2682972 100644
--- a/ts/state/smart/LeftPane.tsx
+++ b/ts/state/smart/LeftPane.tsx
@@ -9,6 +9,7 @@ import type { PropsType as LeftPanePropsType } from '../../components/LeftPane';
import { LeftPane, LeftPaneMode } from '../../components/LeftPane';
import type { StateType } from '../reducer';
import { missingCaseError } from '../../util/missingCaseError';
+import { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid';
import { ComposerStep, OneTimeModalState } from '../ducks/conversationsEnums';
import {
@@ -32,11 +33,11 @@ import {
getComposeGroupName,
getComposerConversationSearchTerm,
getComposerStep,
+ getComposerUUIDFetchState,
getComposeSelectedContacts,
getFilteredCandidateContactsForNewGroup,
getFilteredComposeContacts,
getFilteredComposeGroups,
- getIsFetchingUsername,
getLeftPaneLists,
getMaximumGroupSizeModalState,
getRecommendedGroupSizeModalState,
@@ -141,7 +142,7 @@ const getModeSpecificProps = (
regionCode: getRegionCode(state),
searchTerm: getComposerConversationSearchTerm(state),
isUsernamesEnabled: getUsernamesEnabled(state),
- isFetchingUsername: getIsFetchingUsername(state),
+ uuidFetchState: getComposerUUIDFetchState(state),
};
case ComposerStep.ChooseGroupMembers:
return {
@@ -152,8 +153,10 @@ const getModeSpecificProps = (
OneTimeModalState.Showing,
isShowingMaximumGroupSizeModal:
getMaximumGroupSizeModalState(state) === OneTimeModalState.Showing,
+ regionCode: getRegionCode(state),
searchTerm: getComposerConversationSearchTerm(state),
selectedContacts: getComposeSelectedContacts(state),
+ uuidFetchState: getComposerUUIDFetchState(state),
};
case ComposerStep.SetGroupMetadata:
return {
@@ -192,6 +195,7 @@ const mapStateToProps = (state: StateType) => {
renderUpdateDialog,
renderCaptchaDialog,
renderCrashReportDialog,
+ lookupConversationWithoutUuid,
theme: getTheme(state),
};
};
diff --git a/ts/test-both/helpers/defaultComposerStates.ts b/ts/test-both/helpers/defaultComposerStates.ts
index d4483fd0e33..7fa639ac691 100644
--- a/ts/test-both/helpers/defaultComposerStates.ts
+++ b/ts/test-both/helpers/defaultComposerStates.ts
@@ -7,12 +7,13 @@ import { OneTimeModalState } from '../../groups/toggleSelectedContactForGroupAdd
export const defaultStartDirectConversationComposerState = {
step: ComposerStep.StartDirectConversation as const,
searchTerm: '',
- isFetchingUsername: false,
+ uuidFetchState: {},
};
export const defaultChooseGroupMembersComposerState = {
step: ComposerStep.ChooseGroupMembers as const,
searchTerm: '',
+ uuidFetchState: {},
groupAvatar: undefined,
groupName: '',
groupExpireTimer: 0,
diff --git a/ts/test-both/helpers/fakeLookupConversationWithoutUuid.ts b/ts/test-both/helpers/fakeLookupConversationWithoutUuid.ts
new file mode 100644
index 00000000000..bace33ef9cb
--- /dev/null
+++ b/ts/test-both/helpers/fakeLookupConversationWithoutUuid.ts
@@ -0,0 +1,103 @@
+// Copyright 2022 Signal Messenger, LLC
+// SPDX-License-Identifier: AGPL-3.0-only
+
+import { useState } from 'react';
+
+import type {
+ UUIDFetchStateType,
+ UUIDFetchStateKeyType,
+} from '../../util/uuidFetchState';
+import type { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid';
+import { sleep } from '../../util/sleep';
+import * as durations from '../../util/durations';
+import type { ConversationType } from '../../state/ducks/conversations';
+import { getDefaultConversation } from './getDefaultConversation';
+
+const VALID_IDENTIFIERS = new Set([
+ 'e164:+12125551234',
+ 'username:bobross',
+]);
+
+export function makeFakeLookupConversationWithoutUuid(
+ saveConversation?: (convo: ConversationType) => void
+): typeof lookupConversationWithoutUuid {
+ const cache = new Map();
+
+ return async options => {
+ const identifier: UUIDFetchStateKeyType =
+ options.type === 'e164'
+ ? `e164:${options.e164}`
+ : `username:${options.username}`;
+
+ let result = cache.get(identifier);
+ if (result) {
+ return result.id;
+ }
+
+ if (VALID_IDENTIFIERS.has(identifier) && saveConversation) {
+ result = getDefaultConversation({
+ // We don't really know anything about the contact
+ firstName: undefined,
+ avatarPath: undefined,
+ name: undefined,
+ profileName: undefined,
+
+ ...(options.type === 'e164'
+ ? {
+ title: options.e164,
+ e164: options.e164,
+ phoneNumber: options.e164,
+ }
+ : {
+ title: `@${options.username}`,
+ username: options.username,
+ }),
+ });
+ cache.set(identifier, result);
+
+ saveConversation(result);
+ }
+
+ options.setIsFetchingUUID(identifier, true);
+
+ await sleep(durations.SECOND);
+
+ options.setIsFetchingUUID(identifier, false);
+
+ if (!result) {
+ options.showUserNotFoundModal(
+ options.type === 'username'
+ ? options
+ : {
+ type: 'phoneNumber',
+ phoneNumber: options.phoneNumber,
+ }
+ );
+ return undefined;
+ }
+
+ return result.id;
+ };
+}
+
+type SetIsFetchingUUIDType = (
+ identifier: UUIDFetchStateKeyType,
+ isFetching: boolean
+) => void;
+
+export function useUuidFetchState(
+ initial: UUIDFetchStateType = {}
+): [UUIDFetchStateType, SetIsFetchingUUIDType] {
+ const [uuidFetchState, setUuidFetchState] = useState(initial);
+
+ const setIsFetchingUUID: SetIsFetchingUUIDType = (key, value) => {
+ setUuidFetchState(prev => {
+ return {
+ ...prev,
+ [key]: value,
+ };
+ });
+ };
+
+ return [uuidFetchState, setIsFetchingUUID];
+}
diff --git a/ts/test-both/state/selectors/conversations_test.ts b/ts/test-both/state/selectors/conversations_test.ts
index d5cd035acfa..9fb7b2ec969 100644
--- a/ts/test-both/state/selectors/conversations_test.ts
+++ b/ts/test-both/state/selectors/conversations_test.ts
@@ -977,8 +977,7 @@ describe('both/state/selectors/conversations', () => {
const result = getFilteredComposeContacts(state);
const ids = result.map(contact => contact.id);
- // NOTE: convo-6 matches because you can't write "Sharing" without "in"
- assert.deepEqual(ids, ['convo-1', 'convo-5', 'convo-6']);
+ assert.deepEqual(ids, ['convo-1', 'convo-5']);
});
it('can search for note to self', () => {
diff --git a/ts/test-both/util/filterAndSortConversations_test.ts b/ts/test-both/util/filterAndSortConversations_test.ts
index cf50eb68d14..845fe4672fa 100644
--- a/ts/test-both/util/filterAndSortConversations_test.ts
+++ b/ts/test-both/util/filterAndSortConversations_test.ts
@@ -38,9 +38,11 @@ describe('filterAndSortConversationsByTitle', () => {
];
it('without a search term, sorts conversations by title (but puts no-name contacts at the bottom)', () => {
- const titles = filterAndSortConversationsByTitle(conversations, '').map(
- contact => contact.title
- );
+ const titles = filterAndSortConversationsByTitle(
+ conversations,
+ '',
+ 'US'
+ ).map(contact => contact.title);
assert.deepEqual(titles, [
'Aaron Aardvark',
'Belinda Beetle',
@@ -53,7 +55,8 @@ describe('filterAndSortConversationsByTitle', () => {
it('can search for contacts by title', () => {
const titles = filterAndSortConversationsByTitle(
conversations,
- 'belind'
+ 'belind',
+ 'US'
).map(contact => contact.title);
assert.sameMembers(titles, ['Belinda Beetle', 'Belinda Zephyr']);
});
@@ -61,15 +64,26 @@ describe('filterAndSortConversationsByTitle', () => {
it('can search for contacts by phone number (and puts no-name contacts at the bottom)', () => {
const titles = filterAndSortConversationsByTitle(
conversations,
- '650555'
+ '650555',
+ 'US'
).map(contact => contact.title);
assert.sameMembers(titles, ['Carlos Santana', '+16505551234']);
});
+ it('can search for contacts by formatted phone number (and puts no-name contacts at the bottom)', () => {
+ const titles = filterAndSortConversationsByTitle(
+ conversations,
+ '(650)555 12-34',
+ 'US'
+ ).map(contact => contact.title);
+ assert.sameMembers(titles, ['+16505551234']);
+ });
+
it('can search for contacts by username', () => {
const titles = filterAndSortConversationsByTitle(
conversations,
- 'thisis'
+ 'thisis',
+ 'US'
).map(contact => contact.title);
assert.sameMembers(titles, ['Carlos Santana']);
});
@@ -100,9 +114,11 @@ describe('filterAndSortConversationsByRecent', () => {
];
it('sorts by recency when no search term is provided', () => {
- const titles = filterAndSortConversationsByRecent(conversations, '').map(
- contact => contact.title
- );
+ const titles = filterAndSortConversationsByRecent(
+ conversations,
+ '',
+ 'US'
+ ).map(contact => contact.title);
assert.sameMembers(titles, [
'+16505551234',
'George Washington',
diff --git a/ts/test-node/components/leftPane/LeftPaneChooseGroupMembersHelper_test.ts b/ts/test-node/components/leftPane/LeftPaneChooseGroupMembersHelper_test.ts
index 8f4a776d167..316173b1d39 100644
--- a/ts/test-node/components/leftPane/LeftPaneChooseGroupMembersHelper_test.ts
+++ b/ts/test-node/components/leftPane/LeftPaneChooseGroupMembersHelper_test.ts
@@ -13,10 +13,12 @@ import { updateRemoteConfig } from '../../../test-both/helpers/RemoteConfigStub'
describe('LeftPaneChooseGroupMembersHelper', () => {
const defaults = {
+ uuidFetchState: {},
candidateContacts: [],
isShowingRecommendedGroupSizeModal: false,
isShowingMaximumGroupSizeModal: false,
searchTerm: '',
+ regionCode: 'US',
selectedContacts: [],
};
diff --git a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts
index 375f611f19a..cdac2e02982 100644
--- a/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts
+++ b/ts/test-node/components/leftPane/LeftPaneComposeHelper_test.ts
@@ -29,7 +29,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.strictEqual(helper.getBackAction({ showInbox }), showInbox);
@@ -45,7 +45,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
1
);
@@ -59,7 +59,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
4
);
@@ -73,7 +73,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
7
);
@@ -87,7 +87,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'someone',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
8
);
@@ -101,7 +101,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'someone',
isUsernamesEnabled: false,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
6
);
@@ -115,7 +115,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
2
);
@@ -126,7 +126,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
5
);
@@ -137,13 +137,13 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
7
);
});
- it('returns 1 (for the "Start new conversation" button) if searching for a phone number with no contacts', () => {
+ it('returns 2 (for the "Start new conversation" button) if searching for a phone number with no contacts', () => {
assert.strictEqual(
new LeftPaneComposeHelper({
composeContacts: [],
@@ -151,9 +151,9 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '+16505551234',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
- 1
+ 2
);
});
@@ -165,13 +165,13 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'someone',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
2
);
});
- it('returns the number of contacts + 4 (for the "Start new conversation" button and header) if searching for a phone number', () => {
+ it('returns the number of contacts + 2 (for the "Start new conversation" button and header) if searching for a phone number', () => {
assert.strictEqual(
new LeftPaneComposeHelper({
composeContacts: [getDefaultConversation(), getDefaultConversation()],
@@ -179,9 +179,9 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '+16505551234',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
}).getRowCount(),
- 4
+ 5
);
});
});
@@ -194,7 +194,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.deepEqual(helper.getRow(0), {
@@ -214,7 +214,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.deepEqual(helper.getRow(0), {
@@ -249,7 +249,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.deepEqual(helper.getRow(0), {
@@ -288,7 +288,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: false,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isUndefined(helper.getRow(0));
@@ -306,7 +306,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.deepEqual(helper.getRow(1), {
@@ -324,21 +324,29 @@ describe('LeftPaneComposeHelper', () => {
composeContacts: [],
composeGroups: [],
regionCode: 'US',
- searchTerm: '+16505551234',
+ searchTerm: '+1(650) 555 12 34',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.deepEqual(helper.getRow(0), {
- type: RowType.StartNewConversation,
- phoneNumber: '+16505551234',
+ type: RowType.Header,
+ i18nKey: 'findByPhoneNumberHeader',
});
- assert.isUndefined(helper.getRow(1));
+ assert.deepEqual(helper.getRow(1), {
+ type: RowType.StartNewConversation,
+ phoneNumber: {
+ isValid: true,
+ userInput: '+1(650) 555 12 34',
+ e164: '+16505551234',
+ },
+ isFetching: false,
+ });
+ assert.isUndefined(helper.getRow(2));
});
it('returns just a "find by username" header if no results', () => {
const username = 'someone';
- const isFetchingUsername = true;
const helper = new LeftPaneComposeHelper({
composeContacts: [],
@@ -346,7 +354,9 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: username,
isUsernamesEnabled: true,
- isFetchingUsername,
+ uuidFetchState: {
+ [`username:${username}`]: true,
+ },
});
assert.deepEqual(helper.getRow(0), {
@@ -356,7 +366,7 @@ describe('LeftPaneComposeHelper', () => {
assert.deepEqual(helper.getRow(1), {
type: RowType.UsernameSearchResult,
username,
- isFetchingUsername,
+ isFetchingUsername: true,
});
assert.isUndefined(helper.getRow(2));
});
@@ -370,27 +380,36 @@ describe('LeftPaneComposeHelper', () => {
composeContacts,
composeGroups: [],
regionCode: 'US',
- searchTerm: '+16505551234',
+ searchTerm: '+1(650) 555 12 34',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.deepEqual(helper.getRow(0), {
- type: RowType.StartNewConversation,
- phoneNumber: '+16505551234',
- });
- assert.deepEqual(helper.getRow(1), {
type: RowType.Header,
i18nKey: 'contactsHeader',
});
- assert.deepEqual(helper.getRow(2), {
+ assert.deepEqual(helper.getRow(1), {
type: RowType.Contact,
contact: composeContacts[0],
});
- assert.deepEqual(helper.getRow(3), {
+ assert.deepEqual(helper.getRow(2), {
type: RowType.Contact,
contact: composeContacts[1],
});
+ assert.deepEqual(helper.getRow(3), {
+ type: RowType.Header,
+ i18nKey: 'findByPhoneNumberHeader',
+ });
+ assert.deepEqual(helper.getRow(4), {
+ type: RowType.StartNewConversation,
+ phoneNumber: {
+ isValid: true,
+ userInput: '+1(650) 555 12 34',
+ e164: '+16505551234',
+ },
+ isFetching: false,
+ });
});
});
@@ -402,7 +421,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isUndefined(helper.getConversationAndMessageAtIndex(0));
@@ -417,7 +436,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isUndefined(
@@ -438,7 +457,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isFalse(
@@ -448,7 +467,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'different search',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
assert.isFalse(
@@ -458,7 +477,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'last search',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
});
@@ -470,7 +489,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isFalse(
@@ -480,17 +499,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
- })
- );
- assert.isFalse(
- helper.shouldRecomputeRowHeights({
- composeContacts: [getDefaultConversation()],
- composeGroups: [],
- regionCode: 'US',
- searchTerm: '+16505559876',
- isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
});
@@ -502,7 +511,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isTrue(
@@ -512,7 +521,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
assert.isTrue(
@@ -522,7 +531,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '+16505551234',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
});
@@ -534,7 +543,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: '',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isTrue(
@@ -544,7 +553,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
});
@@ -556,7 +565,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isTrue(
@@ -566,7 +575,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
@@ -576,7 +585,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isTrue(
@@ -586,7 +595,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'foo bar',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
});
@@ -598,7 +607,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'soup',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
});
assert.isTrue(
@@ -608,7 +617,7 @@ describe('LeftPaneComposeHelper', () => {
regionCode: 'US',
searchTerm: 'soup',
isUsernamesEnabled: true,
- isFetchingUsername: false,
+ uuidFetchState: {},
})
);
});
diff --git a/ts/util/filterAndSortConversations.ts b/ts/util/filterAndSortConversations.ts
index c72ac33e952..3c1e7e3d2aa 100644
--- a/ts/util/filterAndSortConversations.ts
+++ b/ts/util/filterAndSortConversations.ts
@@ -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 = {
+const FUSE_OPTIONS: Fuse.IFuseOptions = {
// 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 = {
const collator = new Intl.Collator();
+const cachedIndices = new WeakMap<
+ ReadonlyArray,
+ Fuse
+>();
+
+// See https://fusejs.io/examples.html#extended-search for
+// extended search documentation.
function searchConversations(
conversations: ReadonlyArray,
- searchTerm: string
+ searchTerm: string,
+ regionCode: string | undefined
): Array {
- return new Fuse(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(conversations, FUSE_OPTIONS);
+ cachedIndices.set(conversations, index);
+ }
+
+ const results = index.search(extendedSearchTerm);
+ return results.map(result => result.item);
}
export function filterAndSortConversationsByRecent(
conversations: ReadonlyArray,
- searchTerm: string
+ searchTerm: string,
+ regionCode: string | undefined
): Array {
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,
- searchTerm: string
+ searchTerm: string,
+ regionCode: string | undefined
): Array {
if (searchTerm.length) {
- return searchConversations(conversations, searchTerm);
+ return searchConversations(conversations, searchTerm, regionCode);
}
return conversations.concat().sort((a, b) => {
diff --git a/ts/util/libphonenumberInstance.ts b/ts/util/libphonenumberInstance.ts
index 402ecb48a0b..8edc259070f 100644
--- a/ts/util/libphonenumberInstance.ts
+++ b/ts/util/libphonenumberInstance.ts
@@ -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),
+ };
+}
diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json
index f0d50af50b8..23dd6aa36a2 100644
--- a/ts/util/lint/exceptions.json
+++ b/ts/util/lint/exceptions.json
@@ -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",
diff --git a/ts/util/lookupConversationWithoutUuid.ts b/ts/util/lookupConversationWithoutUuid.ts
new file mode 100644
index 00000000000..069040c111a
--- /dev/null
+++ b/ts/util/lookupConversationWithoutUuid.ts
@@ -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 {
+ 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 {
+ 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;
+ }
+}
diff --git a/ts/util/uuidFetchState.ts b/ts/util/uuidFetchState.ts
new file mode 100644
index 00000000000..474bac9568a
--- /dev/null
+++ b/ts/util/uuidFetchState.ts
@@ -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;
+
+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}`]);
+};
diff --git a/ts/views/inbox_view.ts b/ts/views/inbox_view.ts
index baac4ef07b1..b1f6a408246 100644
--- a/ts/views/inbox_view.ts
+++ b/ts/views/inbox_view.ts
@@ -5,6 +5,7 @@ import * as Backbone from 'backbone';
import * as log from '../logging/log';
import type { ConversationModel } from '../models/conversations';
import { showToast } from '../util/showToast';
+import { strictAssert } from '../util/assert';
import { ToastStickerPackInstallFailed } from '../components/ToastStickerPackInstallFailed';
window.Whisper = window.Whisper || {};
@@ -115,26 +116,19 @@ Whisper.InboxView = Whisper.View.extend({
this.conversation_stack.unload();
});
- window.Whisper.events.on(
- 'showConversation',
- async (id, messageId, username) => {
- const conversation =
- await window.ConversationController.getOrCreateAndWait(
- id,
- 'private',
- { username }
- );
+ window.Whisper.events.on('showConversation', (id, messageId) => {
+ const conversation = window.ConversationController.get(id);
+ strictAssert(conversation, 'Conversation must be found');
- conversation.setMarkedUnread(false);
+ conversation.setMarkedUnread(false);
- const { openConversationExternal } = window.reduxActions.conversations;
- if (openConversationExternal) {
- openConversationExternal(conversation.id, messageId);
- }
-
- this.conversation_stack.open(conversation, messageId);
+ const { openConversationExternal } = window.reduxActions.conversations;
+ if (openConversationExternal) {
+ openConversationExternal(conversation.id, messageId);
}
- );
+
+ this.conversation_stack.open(conversation, messageId);
+ });
window.Whisper.events.on('loadingProgress', count => {
const view = this.appLoadingScreen;
diff --git a/yarn.lock b/yarn.lock
index 2f150f01f23..097ca3c622d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7762,10 +7762,10 @@ functions-have-names@^1.1.1:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.1.1.tgz#79d35927f07b8e7103d819fed475b64ccf7225ea"
integrity sha512-U0kNHUoxwPNPWOJaMG7Z00d4a/qZVrFtzWJRaK8V9goaVOCXBSQSJpt3MYGNtkScKEBKovxLjnNdC9MlXwo5Pw==
-fuse.js@3.4.4:
- version "3.4.4"
- resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.4.tgz#f98f55fcb3b595cf6a3e629c5ffaf10982103e95"
- integrity sha512-pyLQo/1oR5Ywf+a/tY8z4JygnIglmRxVUOiyFAbd11o9keUDpUJSMGRWJngcnkURj30kDHPmhoKY8ChJiz3EpQ==
+fuse.js@6.5.3:
+ version "6.5.3"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.5.3.tgz#7446c0acbc4ab0ab36fa602e97499bdb69452b93"
+ integrity sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg==
fuse.js@^3.4.4:
version "3.4.5"