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
|
@ -824,8 +824,12 @@
|
|||
"description": "Shown to separate the types of search results"
|
||||
},
|
||||
"findByUsernameHeader": {
|
||||
"message": "Find by Username",
|
||||
"description": "Shown to separate the types of search results"
|
||||
"message": "Find by username",
|
||||
"description": "Shown when search could be a valid username, with one sub-item that will kick off the search"
|
||||
},
|
||||
"findByPhoneNumberHeader": {
|
||||
"message": "Find by phone number",
|
||||
"description": "Shown when search could be a valid phone number, with one sub-item that will kick off the search"
|
||||
},
|
||||
"at-username": {
|
||||
"message": "@$username$",
|
||||
|
@ -2352,6 +2356,10 @@
|
|||
"message": "Failed to fetch username. Check your connection and try again.",
|
||||
"description": "Shown if request to Signal servers to find username fails"
|
||||
},
|
||||
"Toast--failed-to-fetch-phone-number": {
|
||||
"message": "Failed to fetch phone number. Check your connection and try again.",
|
||||
"description": "Shown if request to Signal servers to find phone number fails"
|
||||
},
|
||||
"startConversation--username-not-found": {
|
||||
"message": "User not found. $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",
|
||||
|
@ -2362,6 +2370,26 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"startConversation--phone-number-not-found": {
|
||||
"message": "User not found. \"$phoneNumber$\" is not a Signal user.",
|
||||
"description": "Shown in dialog if phone number is not found.",
|
||||
"placeholders": {
|
||||
"phoneNumber": {
|
||||
"content": "$1",
|
||||
"example": "+1 203-123-4567"
|
||||
}
|
||||
}
|
||||
},
|
||||
"startConversation--phone-number-not-valid": {
|
||||
"message": "User not found. \"$phoneNumber$\" is not a valid phone number.",
|
||||
"description": "Shown in dialog if phone number is not valid.",
|
||||
"placeholders": {
|
||||
"phoneNumber": {
|
||||
"content": "$1",
|
||||
"example": "+1 203-123-4567"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chooseGroupMembers__title": {
|
||||
"message": "Choose members",
|
||||
"description": "The title for the 'choose group members' left pane screen"
|
||||
|
|
|
@ -109,7 +109,7 @@
|
|||
"focus-trap-react": "8.8.1",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "5.0.0",
|
||||
"fuse.js": "3.4.4",
|
||||
"fuse.js": "6.5.3",
|
||||
"glob": "7.1.6",
|
||||
"google-libphonenumber": "3.2.27",
|
||||
"got": "11.8.2",
|
||||
|
|
|
@ -4765,6 +4765,11 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__spinner__container {
|
||||
margin-left: 16px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&--header {
|
||||
|
|
|
@ -373,7 +373,7 @@ export class ConversationController {
|
|||
if (normalizedUuid) {
|
||||
newConvo.updateUuid(normalizedUuid);
|
||||
}
|
||||
if (highTrust && e164 && normalizedUuid) {
|
||||
if ((highTrust && e164) || normalizedUuid) {
|
||||
updateConversation(newConvo.attributes);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import enMessages from '../../_locales/en/messages.json';
|
|||
import { ThemeType } from '../types/Util';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
||||
import { UUID } from '../types/UUID';
|
||||
import { makeFakeLookupConversationWithoutUuid } from '../test-both/helpers/fakeLookupConversationWithoutUuid';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -85,13 +86,11 @@ const Wrapper = ({
|
|||
/>
|
||||
)}
|
||||
scrollable={scrollable}
|
||||
lookupConversationWithoutUuid={makeFakeLookupConversationWithoutUuid()}
|
||||
showChooseGroupMembers={action('showChooseGroupMembers')}
|
||||
startNewConversationFromPhoneNumber={action(
|
||||
'startNewConversationFromPhoneNumber'
|
||||
)}
|
||||
startNewConversationFromUsername={action(
|
||||
'startNewConversationFromUsername'
|
||||
)}
|
||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||
setIsFetchingUUID={action('setIsFetchingUUID')}
|
||||
showConversation={action('showConversation')}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
@ -495,16 +494,47 @@ story.add('Headers', () => (
|
|||
type: RowType.Header,
|
||||
i18nKey: 'findByUsernameHeader',
|
||||
},
|
||||
{
|
||||
type: RowType.Header,
|
||||
i18nKey: 'findByPhoneNumberHeader',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Start new conversation', () => (
|
||||
story.add('Find by phone number', () => (
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.Header,
|
||||
i18nKey: 'findByPhoneNumberHeader',
|
||||
},
|
||||
{
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: '+12345559876',
|
||||
phoneNumber: {
|
||||
isValid: true,
|
||||
userInput: '+1(234)555 98 76',
|
||||
e164: '+12345559876',
|
||||
},
|
||||
isFetching: false,
|
||||
},
|
||||
{
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: {
|
||||
isValid: true,
|
||||
userInput: '+1(234)555 98 76',
|
||||
e164: '+12345559876',
|
||||
},
|
||||
isFetching: true,
|
||||
},
|
||||
{
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: {
|
||||
isValid: true,
|
||||
userInput: '+1(234)555',
|
||||
e164: '+1234555',
|
||||
},
|
||||
isFetching: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
@ -548,7 +578,30 @@ story.add('Kitchen sink', () => (
|
|||
rows={[
|
||||
{
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: '+12345559876',
|
||||
phoneNumber: {
|
||||
isValid: true,
|
||||
userInput: '+1(234)555 98 76',
|
||||
e164: '+12345559876',
|
||||
},
|
||||
isFetching: false,
|
||||
},
|
||||
{
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: {
|
||||
isValid: true,
|
||||
userInput: '+1(234)555 98 76',
|
||||
e164: '+12345559876',
|
||||
},
|
||||
isFetching: true,
|
||||
},
|
||||
{
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: {
|
||||
isValid: false,
|
||||
userInput: '+1(234)555 98',
|
||||
e164: '+123455598',
|
||||
},
|
||||
isFetching: true,
|
||||
},
|
||||
{
|
||||
type: RowType.Header,
|
||||
|
|
|
@ -10,10 +10,12 @@ import { get, pick } from 'lodash';
|
|||
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { assert } from '../util/assert';
|
||||
import type { ParsedE164Type } from '../util/libphonenumberInstance';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import { ScrollBehavior } from '../types/Util';
|
||||
import { getConversationListWidthBreakpoint } from './_util';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid';
|
||||
|
||||
import type { PropsData as ConversationListItemPropsType } from './conversationList/ConversationListItem';
|
||||
import { ConversationListItem } from './conversationList/ConversationListItem';
|
||||
|
@ -21,6 +23,7 @@ import type { ContactListItemConversationType as ContactListItemPropsType } from
|
|||
import { ContactListItem } from './conversationList/ContactListItem';
|
||||
import type { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
|
||||
import { ContactCheckbox as ContactCheckboxComponent } from './conversationList/ContactCheckbox';
|
||||
import { PhoneNumberCheckbox as PhoneNumberCheckboxComponent } from './conversationList/PhoneNumberCheckbox';
|
||||
import { CreateNewGroupButton } from './conversationList/CreateNewGroupButton';
|
||||
import { StartNewConversation as StartNewConversationComponent } from './conversationList/StartNewConversation';
|
||||
import { SearchResultsLoadingFakeHeader as SearchResultsLoadingFakeHeaderComponent } from './conversationList/SearchResultsLoadingFakeHeader';
|
||||
|
@ -32,6 +35,7 @@ export enum RowType {
|
|||
Blank,
|
||||
Contact,
|
||||
ContactCheckbox,
|
||||
PhoneNumberCheckbox,
|
||||
Conversation,
|
||||
CreateNewGroup,
|
||||
Header,
|
||||
|
@ -62,6 +66,13 @@ type ContactCheckboxRowType = {
|
|||
disabledReason?: ContactCheckboxDisabledReason;
|
||||
};
|
||||
|
||||
type PhoneNumberCheckboxRowType = {
|
||||
type: RowType.PhoneNumberCheckbox;
|
||||
phoneNumber: ParsedE164Type;
|
||||
isChecked: boolean;
|
||||
isFetching: boolean;
|
||||
};
|
||||
|
||||
type ConversationRowType = {
|
||||
type: RowType.Conversation;
|
||||
conversation: ConversationListItemPropsType;
|
||||
|
@ -91,7 +102,8 @@ type SearchResultsLoadingFakeRowType = {
|
|||
|
||||
type StartNewConversationRowType = {
|
||||
type: RowType.StartNewConversation;
|
||||
phoneNumber: string;
|
||||
phoneNumber: ParsedE164Type;
|
||||
isFetching: boolean;
|
||||
};
|
||||
|
||||
type UsernameRowType = {
|
||||
|
@ -105,6 +117,7 @@ export type Row =
|
|||
| BlankRowType
|
||||
| ContactRowType
|
||||
| ContactCheckboxRowType
|
||||
| PhoneNumberCheckboxRowType
|
||||
| ConversationRowType
|
||||
| CreateNewGroupRowType
|
||||
| MessageRowType
|
||||
|
@ -141,9 +154,8 @@ export type PropsType = {
|
|||
onSelectConversation: (conversationId: string, messageId?: string) => void;
|
||||
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||
showChooseGroupMembers: () => void;
|
||||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||
startNewConversationFromUsername: (username: string) => void;
|
||||
};
|
||||
showConversation: (conversationId: string) => void;
|
||||
} & LookupConversationWithoutUuidActionsType;
|
||||
|
||||
const NORMAL_ROW_HEIGHT = 76;
|
||||
const HEADER_ROW_HEIGHT = 40;
|
||||
|
@ -163,8 +175,10 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
scrollable = true,
|
||||
shouldRecomputeRowHeights,
|
||||
showChooseGroupMembers,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startNewConversationFromUsername,
|
||||
lookupConversationWithoutUuid,
|
||||
showUserNotFoundModal,
|
||||
setIsFetchingUUID,
|
||||
showConversation,
|
||||
theme,
|
||||
}) => {
|
||||
const listRef = useRef<null | List>(null);
|
||||
|
@ -251,6 +265,23 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
/>
|
||||
);
|
||||
break;
|
||||
case RowType.PhoneNumberCheckbox:
|
||||
result = (
|
||||
<PhoneNumberCheckboxComponent
|
||||
phoneNumber={row.phoneNumber}
|
||||
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
|
||||
showUserNotFoundModal={showUserNotFoundModal}
|
||||
setIsFetchingUUID={setIsFetchingUUID}
|
||||
toggleConversationInChooseMembers={conversationId =>
|
||||
onClickContactCheckbox(conversationId, undefined)
|
||||
}
|
||||
isChecked={row.isChecked}
|
||||
isFetching={row.isFetching}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case RowType.Conversation: {
|
||||
const itemProps = pick(row.conversation, [
|
||||
'acceptedMessageRequest',
|
||||
|
@ -332,7 +363,11 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
<StartNewConversationComponent
|
||||
i18n={i18n}
|
||||
phoneNumber={row.phoneNumber}
|
||||
onClick={startNewConversationFromPhoneNumber}
|
||||
isFetching={row.isFetching}
|
||||
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
|
||||
showUserNotFoundModal={showUserNotFoundModal}
|
||||
setIsFetchingUUID={setIsFetchingUUID}
|
||||
showConversation={showConversation}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -342,7 +377,10 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
i18n={i18n}
|
||||
username={row.username}
|
||||
isFetchingUsername={row.isFetchingUsername}
|
||||
onClick={startNewConversationFromUsername}
|
||||
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
|
||||
showUserNotFoundModal={showUserNotFoundModal}
|
||||
setIsFetchingUUID={setIsFetchingUUID}
|
||||
showConversation={showConversation}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
@ -365,10 +403,12 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
onClickArchiveButton,
|
||||
onClickContactCheckbox,
|
||||
onSelectConversation,
|
||||
lookupConversationWithoutUuid,
|
||||
showUserNotFoundModal,
|
||||
setIsFetchingUUID,
|
||||
renderMessageSearchResult,
|
||||
showChooseGroupMembers,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startNewConversationFromUsername,
|
||||
showConversation,
|
||||
theme,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -60,6 +60,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
removeLinkPreview: action('removeLinkPreview'),
|
||||
skinTone: 0,
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
regionCode: 'US',
|
||||
});
|
||||
|
||||
story.add('Modal', () => {
|
||||
|
|
|
@ -60,6 +60,7 @@ export type DataPropsType = {
|
|||
) => unknown;
|
||||
onTextTooLong: () => void;
|
||||
theme: ThemeType;
|
||||
regionCode: string | undefined;
|
||||
} & Pick<EmojiButtonProps, 'recentEmojis' | 'skinTone'>;
|
||||
|
||||
type ActionPropsType = Pick<
|
||||
|
@ -91,6 +92,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
removeLinkPreview,
|
||||
skinTone,
|
||||
theme,
|
||||
regionCode,
|
||||
}) => {
|
||||
const inputRef = useRef<null | HTMLInputElement>(null);
|
||||
const inputApiRef = React.useRef<InputApi | undefined>();
|
||||
|
@ -99,7 +101,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filteredConversations, setFilteredConversations] = useState(
|
||||
filterAndSortConversationsByRecent(candidateConversations, '')
|
||||
filterAndSortConversationsByRecent(candidateConversations, '', regionCode)
|
||||
);
|
||||
const [attachmentsToForward, setAttachmentsToForward] = useState<
|
||||
Array<AttachmentType>
|
||||
|
@ -168,14 +170,20 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
setFilteredConversations(
|
||||
filterAndSortConversationsByRecent(
|
||||
candidateConversations,
|
||||
normalizedSearchTerm
|
||||
normalizedSearchTerm,
|
||||
regionCode
|
||||
)
|
||||
);
|
||||
}, 200);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [candidateConversations, normalizedSearchTerm, setFilteredConversations]);
|
||||
}, [
|
||||
candidateConversations,
|
||||
normalizedSearchTerm,
|
||||
setFilteredConversations,
|
||||
regionCode,
|
||||
]);
|
||||
|
||||
const contactLookup = useMemo(() => {
|
||||
const map = new Map();
|
||||
|
@ -412,6 +420,12 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
toggleSelectedConversation(conversationId);
|
||||
}
|
||||
}}
|
||||
lookupConversationWithoutUuid={
|
||||
asyncShouldNeverBeCalled
|
||||
}
|
||||
showConversation={shouldNeverBeCalled}
|
||||
showUserNotFoundModal={shouldNeverBeCalled}
|
||||
setIsFetchingUUID={shouldNeverBeCalled}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
renderMessageSearchResult={() => {
|
||||
shouldNeverBeCalled();
|
||||
|
@ -420,10 +434,6 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
rowCount={rowCount}
|
||||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
startNewConversationFromPhoneNumber={
|
||||
shouldNeverBeCalled
|
||||
}
|
||||
startNewConversationFromUsername={shouldNeverBeCalled}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
|
@ -470,3 +480,11 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
function shouldNeverBeCalled(..._args: ReadonlyArray<unknown>): void {
|
||||
assert(false, 'This should never be called. Doing nothing');
|
||||
}
|
||||
|
||||
async function asyncShouldNeverBeCalled(
|
||||
..._args: ReadonlyArray<unknown>
|
||||
): Promise<undefined> {
|
||||
shouldNeverBeCalled();
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
import React from 'react';
|
||||
import type {
|
||||
ContactModalStateType,
|
||||
UsernameNotFoundModalStateType,
|
||||
UserNotFoundModalStateType,
|
||||
} from '../state/ducks/globalModals';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
import { ButtonVariant } from './Button';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
|
@ -23,9 +24,9 @@ type PropsType = {
|
|||
// SafetyNumberModal
|
||||
safetyNumberModalContactId?: string;
|
||||
renderSafetyNumber: () => JSX.Element;
|
||||
// UsernameNotFoundModal
|
||||
hideUsernameNotFoundModal: () => unknown;
|
||||
usernameNotFoundModalState?: UsernameNotFoundModalStateType;
|
||||
// UserNotFoundModal
|
||||
hideUserNotFoundModal: () => unknown;
|
||||
userNotFoundModalState?: UserNotFoundModalStateType;
|
||||
// WhatsNewModal
|
||||
isWhatsNewVisible: boolean;
|
||||
hideWhatsNewModal: () => unknown;
|
||||
|
@ -42,9 +43,9 @@ export const GlobalModalContainer = ({
|
|||
// SafetyNumberModal
|
||||
safetyNumberModalContactId,
|
||||
renderSafetyNumber,
|
||||
// UsernameNotFoundModal
|
||||
hideUsernameNotFoundModal,
|
||||
usernameNotFoundModalState,
|
||||
// UserNotFoundModal
|
||||
hideUserNotFoundModal,
|
||||
userNotFoundModalState,
|
||||
// WhatsNewModal
|
||||
hideWhatsNewModal,
|
||||
isWhatsNewVisible,
|
||||
|
@ -53,19 +54,30 @@ export const GlobalModalContainer = ({
|
|||
return renderSafetyNumber();
|
||||
}
|
||||
|
||||
if (usernameNotFoundModalState) {
|
||||
if (userNotFoundModalState) {
|
||||
let content: string;
|
||||
if (userNotFoundModalState.type === 'phoneNumber') {
|
||||
content = i18n('startConversation--phone-number-not-found', {
|
||||
phoneNumber: userNotFoundModalState.phoneNumber,
|
||||
});
|
||||
} else if (userNotFoundModalState.type === 'username') {
|
||||
content = i18n('startConversation--username-not-found', {
|
||||
atUsername: i18n('at-username', {
|
||||
username: userNotFoundModalState.username,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
throw missingCaseError(userNotFoundModalState);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
cancelText={i18n('ok')}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
i18n={i18n}
|
||||
onClose={hideUsernameNotFoundModal}
|
||||
onClose={hideUserNotFoundModal}
|
||||
>
|
||||
{i18n('startConversation--username-not-found', {
|
||||
atUsername: i18n('at-username', {
|
||||
username: usernameNotFoundModalState.username,
|
||||
}),
|
||||
})}
|
||||
{content}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,6 +18,10 @@ import enMessages from '../../_locales/en/messages.json';
|
|||
import { ThemeType } from '../types/Util';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
||||
import {
|
||||
makeFakeLookupConversationWithoutUuid,
|
||||
useUuidFetchState,
|
||||
} from '../test-both/helpers/fakeLookupConversationWithoutUuid';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -89,93 +93,112 @@ const defaultModeSpecificProps = {
|
|||
|
||||
const emptySearchResultsGroup = { isLoading: false, results: [] };
|
||||
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
clearConversationSearch: action('clearConversationSearch'),
|
||||
clearGroupCreationError: action('clearGroupCreationError'),
|
||||
clearSearch: action('clearSearch'),
|
||||
closeMaximumGroupSizeModal: action('closeMaximumGroupSizeModal'),
|
||||
closeRecommendedGroupSizeModal: action('closeRecommendedGroupSizeModal'),
|
||||
composeDeleteAvatarFromDisk: action('composeDeleteAvatarFromDisk'),
|
||||
composeReplaceAvatar: action('composeReplaceAvatar'),
|
||||
composeSaveAvatarToDisk: action('composeSaveAvatarToDisk'),
|
||||
createGroup: action('createGroup'),
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
modeSpecificProps: defaultModeSpecificProps,
|
||||
preferredWidthFromStorage: 320,
|
||||
openConversationInternal: action('openConversationInternal'),
|
||||
regionCode: 'US',
|
||||
challengeStatus: select(
|
||||
'challengeStatus',
|
||||
['idle', 'required', 'pending'],
|
||||
'idle'
|
||||
),
|
||||
crashReportCount: select('challengeReportCount', [0, 1], 0),
|
||||
setChallengeStatus: action('setChallengeStatus'),
|
||||
renderExpiredBuildDialog: () => <div />,
|
||||
renderMainHeader: () => <div />,
|
||||
renderMessageSearchResult: (id: string) => (
|
||||
<MessageSearchResult
|
||||
body="Lorem ipsum wow"
|
||||
bodyRanges={[]}
|
||||
conversationId="marc-convo"
|
||||
from={defaultConversations[0]}
|
||||
getPreferredBadge={() => undefined}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
openConversationInternal={action('openConversationInternal')}
|
||||
sentAt={1587358800000}
|
||||
snippet="Lorem <<left>>ipsum<<right>> wow"
|
||||
theme={ThemeType.light}
|
||||
to={defaultConversations[1]}
|
||||
/>
|
||||
),
|
||||
renderNetworkStatus: () => <div />,
|
||||
renderRelinkDialog: () => <div />,
|
||||
renderUpdateDialog: () => <div />,
|
||||
renderCaptchaDialog: () => (
|
||||
<CaptchaDialog
|
||||
i18n={i18n}
|
||||
isPending={overrideProps.challengeStatus === 'pending'}
|
||||
onContinue={action('onCaptchaContinue')}
|
||||
onSkip={action('onCaptchaSkip')}
|
||||
/>
|
||||
),
|
||||
renderCrashReportDialog: () => (
|
||||
<CrashReportDialog
|
||||
i18n={i18n}
|
||||
isPending={false}
|
||||
uploadCrashReports={action('uploadCrashReports')}
|
||||
eraseCrashReports={action('eraseCrashReports')}
|
||||
/>
|
||||
),
|
||||
selectedConversationId: undefined,
|
||||
selectedMessageId: undefined,
|
||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
setComposeSearchTerm: action('setComposeSearchTerm'),
|
||||
setComposeGroupAvatar: action('setComposeGroupAvatar'),
|
||||
setComposeGroupName: action('setComposeGroupName'),
|
||||
setComposeGroupExpireTimer: action('setComposeGroupExpireTimer'),
|
||||
showArchivedConversations: action('showArchivedConversations'),
|
||||
showInbox: action('showInbox'),
|
||||
startComposing: action('startComposing'),
|
||||
showChooseGroupMembers: action('showChooseGroupMembers'),
|
||||
startNewConversationFromPhoneNumber: action(
|
||||
'startNewConversationFromPhoneNumber'
|
||||
),
|
||||
startNewConversationFromUsername: action('startNewConversationFromUsername'),
|
||||
startSearch: action('startSearch'),
|
||||
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
toggleComposeEditingAvatar: action('toggleComposeEditingAvatar'),
|
||||
toggleConversationInChooseMembers: action(
|
||||
'toggleConversationInChooseMembers'
|
||||
),
|
||||
updateSearchTerm: action('updateSearchTerm'),
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
||||
let modeSpecificProps =
|
||||
overrideProps.modeSpecificProps ?? defaultModeSpecificProps;
|
||||
|
||||
...overrideProps,
|
||||
});
|
||||
const [uuidFetchState, setIsFetchingUUID] = useUuidFetchState(
|
||||
'uuidFetchState' in modeSpecificProps
|
||||
? modeSpecificProps.uuidFetchState
|
||||
: {}
|
||||
);
|
||||
|
||||
if ('uuidFetchState' in modeSpecificProps) {
|
||||
modeSpecificProps = {
|
||||
...modeSpecificProps,
|
||||
uuidFetchState,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
clearConversationSearch: action('clearConversationSearch'),
|
||||
clearGroupCreationError: action('clearGroupCreationError'),
|
||||
clearSearch: action('clearSearch'),
|
||||
closeMaximumGroupSizeModal: action('closeMaximumGroupSizeModal'),
|
||||
closeRecommendedGroupSizeModal: action('closeRecommendedGroupSizeModal'),
|
||||
composeDeleteAvatarFromDisk: action('composeDeleteAvatarFromDisk'),
|
||||
composeReplaceAvatar: action('composeReplaceAvatar'),
|
||||
composeSaveAvatarToDisk: action('composeSaveAvatarToDisk'),
|
||||
createGroup: action('createGroup'),
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
preferredWidthFromStorage: 320,
|
||||
openConversationInternal: action('openConversationInternal'),
|
||||
regionCode: 'US',
|
||||
challengeStatus: select(
|
||||
'challengeStatus',
|
||||
['idle', 'required', 'pending'],
|
||||
'idle'
|
||||
),
|
||||
crashReportCount: select('challengeReportCount', [0, 1], 0),
|
||||
setChallengeStatus: action('setChallengeStatus'),
|
||||
lookupConversationWithoutUuid: makeFakeLookupConversationWithoutUuid(),
|
||||
showUserNotFoundModal: action('showUserNotFoundModal'),
|
||||
setIsFetchingUUID,
|
||||
showConversation: action('showConversation'),
|
||||
renderExpiredBuildDialog: () => <div />,
|
||||
renderMainHeader: () => <div />,
|
||||
renderMessageSearchResult: (id: string) => (
|
||||
<MessageSearchResult
|
||||
body="Lorem ipsum wow"
|
||||
bodyRanges={[]}
|
||||
conversationId="marc-convo"
|
||||
from={defaultConversations[0]}
|
||||
getPreferredBadge={() => undefined}
|
||||
i18n={i18n}
|
||||
id={id}
|
||||
openConversationInternal={action('openConversationInternal')}
|
||||
sentAt={1587358800000}
|
||||
snippet="Lorem <<left>>ipsum<<right>> wow"
|
||||
theme={ThemeType.light}
|
||||
to={defaultConversations[1]}
|
||||
/>
|
||||
),
|
||||
renderNetworkStatus: () => <div />,
|
||||
renderRelinkDialog: () => <div />,
|
||||
renderUpdateDialog: () => <div />,
|
||||
renderCaptchaDialog: () => (
|
||||
<CaptchaDialog
|
||||
i18n={i18n}
|
||||
isPending={overrideProps.challengeStatus === 'pending'}
|
||||
onContinue={action('onCaptchaContinue')}
|
||||
onSkip={action('onCaptchaSkip')}
|
||||
/>
|
||||
),
|
||||
renderCrashReportDialog: () => (
|
||||
<CrashReportDialog
|
||||
i18n={i18n}
|
||||
isPending={false}
|
||||
uploadCrashReports={action('uploadCrashReports')}
|
||||
eraseCrashReports={action('eraseCrashReports')}
|
||||
/>
|
||||
),
|
||||
selectedConversationId: undefined,
|
||||
selectedMessageId: undefined,
|
||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
setComposeSearchTerm: action('setComposeSearchTerm'),
|
||||
setComposeGroupAvatar: action('setComposeGroupAvatar'),
|
||||
setComposeGroupName: action('setComposeGroupName'),
|
||||
setComposeGroupExpireTimer: action('setComposeGroupExpireTimer'),
|
||||
showArchivedConversations: action('showArchivedConversations'),
|
||||
showInbox: action('showInbox'),
|
||||
startComposing: action('startComposing'),
|
||||
showChooseGroupMembers: action('showChooseGroupMembers'),
|
||||
startSearch: action('startSearch'),
|
||||
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
toggleComposeEditingAvatar: action('toggleComposeEditingAvatar'),
|
||||
toggleConversationInChooseMembers: action(
|
||||
'toggleConversationInChooseMembers'
|
||||
),
|
||||
updateSearchTerm: action('updateSearchTerm'),
|
||||
|
||||
...overrideProps,
|
||||
|
||||
modeSpecificProps,
|
||||
};
|
||||
};
|
||||
|
||||
// Inbox stories
|
||||
|
||||
|
@ -465,7 +488,7 @@ story.add('Compose: no results', () => (
|
|||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: '',
|
||||
},
|
||||
|
@ -481,7 +504,7 @@ story.add('Compose: some contacts, no search term', () => (
|
|||
composeContacts: defaultConversations,
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: '',
|
||||
},
|
||||
|
@ -497,7 +520,7 @@ story.add('Compose: some contacts, with a search term', () => (
|
|||
composeContacts: defaultConversations,
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: 'ar',
|
||||
},
|
||||
|
@ -513,7 +536,7 @@ story.add('Compose: some groups, no search term', () => (
|
|||
composeContacts: [],
|
||||
composeGroups: defaultGroups,
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: '',
|
||||
},
|
||||
|
@ -529,7 +552,7 @@ story.add('Compose: some groups, with search term', () => (
|
|||
composeContacts: [],
|
||||
composeGroups: defaultGroups,
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: 'ar',
|
||||
},
|
||||
|
@ -545,7 +568,7 @@ story.add('Compose: search is valid username', () => (
|
|||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: 'someone',
|
||||
},
|
||||
|
@ -561,7 +584,9 @@ story.add('Compose: search is valid username, fetching username', () => (
|
|||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: true,
|
||||
uuidFetchState: {
|
||||
'username:someone': true,
|
||||
},
|
||||
regionCode: 'US',
|
||||
searchTerm: 'someone',
|
||||
},
|
||||
|
@ -577,7 +602,7 @@ story.add('Compose: search is valid username, but flag is not enabled', () => (
|
|||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: false,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: 'someone',
|
||||
},
|
||||
|
@ -585,6 +610,59 @@ story.add('Compose: search is valid username, but flag is not enabled', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: search is partial phone number', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: '+1(212)555',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: search is valid phone number', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: '2125555454',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add(
|
||||
'Compose: search is valid phone number, fetching phone number',
|
||||
() => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
uuidFetchState: {
|
||||
'e164:+12125555454': true,
|
||||
},
|
||||
regionCode: 'US',
|
||||
searchTerm: '(212)5555454',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
story.add('Compose: all kinds of results, no search term', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
|
@ -593,7 +671,7 @@ story.add('Compose: all kinds of results, no search term', () => (
|
|||
composeContacts: defaultConversations,
|
||||
composeGroups: defaultGroups,
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: '',
|
||||
},
|
||||
|
@ -609,7 +687,7 @@ story.add('Compose: all kinds of results, with a search term', () => (
|
|||
composeContacts: defaultConversations,
|
||||
composeGroups: defaultGroups,
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
uuidFetchState: {},
|
||||
regionCode: 'US',
|
||||
searchTerm: 'someone',
|
||||
},
|
||||
|
@ -672,6 +750,42 @@ story.add('Crash report dialog', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
// Choose Group Members
|
||||
|
||||
story.add('Choose Group Members: Partial phone number', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.ChooseGroupMembers,
|
||||
uuidFetchState: {},
|
||||
candidateContacts: [],
|
||||
isShowingRecommendedGroupSizeModal: false,
|
||||
isShowingMaximumGroupSizeModal: false,
|
||||
searchTerm: '+1(212) 555',
|
||||
regionCode: 'US',
|
||||
selectedContacts: [],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Choose Group Members: Valid phone number', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.ChooseGroupMembers,
|
||||
uuidFetchState: {},
|
||||
candidateContacts: [],
|
||||
isShowingRecommendedGroupSizeModal: false,
|
||||
isShowingMaximumGroupSizeModal: false,
|
||||
searchTerm: '+1(212) 555 5454',
|
||||
regionCode: 'US',
|
||||
selectedContacts: [],
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
// Set group metadata
|
||||
|
||||
story.add('Group Metadata: No Timer', () => (
|
||||
|
|
|
@ -37,6 +37,7 @@ import {
|
|||
MAX_WIDTH,
|
||||
getWidthFromPreferredWidth,
|
||||
} from '../util/leftPaneWidth';
|
||||
import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid';
|
||||
|
||||
import { ConversationList } from './ConversationList';
|
||||
import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
|
||||
|
@ -97,8 +98,6 @@ export type PropsType = {
|
|||
closeMaximumGroupSizeModal: () => void;
|
||||
closeRecommendedGroupSizeModal: () => void;
|
||||
createGroup: () => void;
|
||||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||
startNewConversationFromUsername: (username: string) => void;
|
||||
openConversationInternal: (_: {
|
||||
conversationId: string;
|
||||
messageId?: string;
|
||||
|
@ -140,7 +139,9 @@ export type PropsType = {
|
|||
) => JSX.Element;
|
||||
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
||||
renderCrashReportDialog: () => JSX.Element;
|
||||
};
|
||||
|
||||
showConversation: (conversationId: string) => void;
|
||||
} & LookupConversationWithoutUuidActionsType;
|
||||
|
||||
export const LeftPane: React.FC<PropsType> = ({
|
||||
challengeStatus,
|
||||
|
@ -181,12 +182,14 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
showInbox,
|
||||
startComposing,
|
||||
startSearch,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startNewConversationFromUsername,
|
||||
showUserNotFoundModal,
|
||||
setIsFetchingUUID,
|
||||
lookupConversationWithoutUuid,
|
||||
toggleConversationInChooseMembers,
|
||||
showConversation,
|
||||
startSettingGroupMetadata,
|
||||
theme,
|
||||
toggleComposeEditingAvatar,
|
||||
toggleConversationInChooseMembers,
|
||||
updateSearchTerm,
|
||||
}) => {
|
||||
const [preferredWidth, setPreferredWidth] = useState(
|
||||
|
@ -599,6 +602,10 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
throw missingCaseError(disabledReason);
|
||||
}
|
||||
}}
|
||||
showUserNotFoundModal={showUserNotFoundModal}
|
||||
setIsFetchingUUID={setIsFetchingUUID}
|
||||
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
|
||||
showConversation={showConversation}
|
||||
onSelectConversation={onSelectConversation}
|
||||
renderMessageSearchResult={renderMessageSearchResult}
|
||||
rowCount={helper.getRowCount()}
|
||||
|
@ -607,12 +614,6 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
scrollable={isScrollable}
|
||||
shouldRecomputeRowHeights={shouldRecomputeRowHeights}
|
||||
showChooseGroupMembers={showChooseGroupMembers}
|
||||
startNewConversationFromPhoneNumber={
|
||||
startNewConversationFromPhoneNumber
|
||||
}
|
||||
startNewConversationFromUsername={
|
||||
startNewConversationFromUsername
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { FuseOptions } from 'fuse.js';
|
||||
import Fuse from 'fuse.js';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isNotNil } from '../util/isNotNil';
|
||||
import type { ConversationStoryType, StoryViewType } from './StoryListItem';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { StoryListItem } from './StoryListItem';
|
||||
|
||||
const FUSE_OPTIONS: FuseOptions<ConversationStoryType> = {
|
||||
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationStoryType> = {
|
||||
getFn: (obj, path) => {
|
||||
if (path === 'searchNames') {
|
||||
return obj.stories.flatMap((story: StoryViewType) => [
|
||||
story.sender.title,
|
||||
story.sender.name,
|
||||
]);
|
||||
return obj.stories
|
||||
.flatMap((story: StoryViewType) => [
|
||||
story.sender.title,
|
||||
story.sender.name,
|
||||
])
|
||||
.filter(isNotNil);
|
||||
}
|
||||
|
||||
return obj.group?.title;
|
||||
return obj.group?.title ?? '';
|
||||
},
|
||||
keys: [
|
||||
{
|
||||
|
@ -32,16 +34,15 @@ const FUSE_OPTIONS: FuseOptions<ConversationStoryType> = {
|
|||
},
|
||||
],
|
||||
threshold: 0.1,
|
||||
tokenize: true,
|
||||
};
|
||||
|
||||
function search(
|
||||
stories: ReadonlyArray<ConversationStoryType>,
|
||||
searchTerm: string
|
||||
): Array<ConversationStoryType> {
|
||||
return new Fuse<ConversationStoryType>(stories, FUSE_OPTIONS).search(
|
||||
searchTerm
|
||||
);
|
||||
return new Fuse<ConversationStoryType>(stories, FUSE_OPTIONS)
|
||||
.search(searchTerm)
|
||||
.map(result => result.item);
|
||||
}
|
||||
|
||||
function getNewestStory(story: ConversationStoryType): StoryViewType {
|
||||
|
|
22
ts/components/ToastFailedToFetchPhoneNumber.tsx
Normal file
22
ts/components/ToastFailedToFetchPhoneNumber.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Toast } from './Toast';
|
||||
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
onClose: () => unknown;
|
||||
};
|
||||
|
||||
export const ToastFailedToFetchPhoneNumber = ({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element => {
|
||||
return (
|
||||
<Toast onClose={onClose} style={{ maxWidth: '280px' }}>
|
||||
{i18n('Toast--failed-to-fetch-phone-number')}
|
||||
</Toast>
|
||||
);
|
||||
};
|
|
@ -9,12 +9,18 @@ import { storiesOf } from '@storybook/react';
|
|||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { sleep } from '../../../util/sleep';
|
||||
import { makeLookup } from '../../../util/makeLookup';
|
||||
import { deconstructLookup } from '../../../util/deconstructLookup';
|
||||
import { setupI18n } from '../../../util/setupI18n';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import enMessages from '../../../../_locales/en/messages.json';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import { AddGroupMembersModal } from './AddGroupMembersModal';
|
||||
import { ChooseGroupMembersModal } from './AddGroupMembersModal/ChooseGroupMembersModal';
|
||||
import { ConfirmAdditionsModal } from './AddGroupMembersModal/ConfirmAdditionsModal';
|
||||
import { RequestState } from './util';
|
||||
import { ThemeType } from '../../../types/Util';
|
||||
import { makeFakeLookupConversationWithoutUuid } from '../../../test-both/helpers/fakeLookupConversationWithoutUuid';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -24,14 +30,23 @@ const story = storiesOf(
|
|||
);
|
||||
|
||||
const allCandidateContacts = times(50, () => getDefaultConversation());
|
||||
let allCandidateContactsLookup = makeLookup(allCandidateContacts, 'id');
|
||||
|
||||
const lookupConversationWithoutUuid = makeFakeLookupConversationWithoutUuid(
|
||||
convo => {
|
||||
allCandidateContacts.push(convo);
|
||||
allCandidateContactsLookup = makeLookup(allCandidateContacts, 'id');
|
||||
}
|
||||
);
|
||||
|
||||
type PropsType = ComponentProps<typeof AddGroupMembersModal>;
|
||||
|
||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||
candidateContacts: allCandidateContacts,
|
||||
const createProps = (
|
||||
overrideProps: Partial<PropsType> = {},
|
||||
candidateContacts: Array<ConversationType> = []
|
||||
): PropsType => ({
|
||||
clearRequestError: action('clearRequestError'),
|
||||
conversationIdsAlreadyInGroup: new Set(),
|
||||
getPreferredBadge: () => undefined,
|
||||
groupTitle: 'Tahoe Trip',
|
||||
i18n,
|
||||
onClose: action('onClose'),
|
||||
|
@ -39,7 +54,38 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
action('onMakeRequest')(conversationIds);
|
||||
},
|
||||
requestState: RequestState.Inactive,
|
||||
theme: ThemeType.light,
|
||||
renderChooseGroupMembersModal: props => {
|
||||
const { selectedConversationIds } = props;
|
||||
return (
|
||||
<ChooseGroupMembersModal
|
||||
{...props}
|
||||
candidateContacts={candidateContacts}
|
||||
selectedContacts={deconstructLookup(
|
||||
allCandidateContactsLookup,
|
||||
selectedConversationIds
|
||||
)}
|
||||
regionCode="US"
|
||||
getPreferredBadge={() => undefined}
|
||||
theme={ThemeType.light}
|
||||
i18n={i18n}
|
||||
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
|
||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderConfirmAdditionsModal: props => {
|
||||
const { selectedConversationIds } = props;
|
||||
return (
|
||||
<ConfirmAdditionsModal
|
||||
{...props}
|
||||
i18n={i18n}
|
||||
selectedContacts={deconstructLookup(
|
||||
allCandidateContactsLookup,
|
||||
selectedConversationIds
|
||||
)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
...overrideProps,
|
||||
});
|
||||
|
||||
|
@ -47,18 +93,12 @@ story.add('Default', () => <AddGroupMembersModal {...createProps()} />);
|
|||
|
||||
story.add('Only 3 contacts', () => (
|
||||
<AddGroupMembersModal
|
||||
{...createProps({
|
||||
candidateContacts: allCandidateContacts.slice(0, 3),
|
||||
})}
|
||||
{...createProps({}, allCandidateContacts.slice(0, 3))}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('No candidate contacts', () => (
|
||||
<AddGroupMembersModal
|
||||
{...createProps({
|
||||
candidateContacts: [],
|
||||
})}
|
||||
/>
|
||||
<AddGroupMembersModal {...createProps({}, [])} />
|
||||
));
|
||||
|
||||
story.add('Everyone already added', () => (
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React, { useMemo, useReducer } from 'react';
|
||||
import React, { useReducer } from 'react';
|
||||
import { without } from 'lodash';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../../types/Util';
|
||||
import type { LocalizerType } from '../../../types/Util';
|
||||
import {
|
||||
AddGroupMemberErrorDialog,
|
||||
AddGroupMemberErrorDialogMode,
|
||||
} from '../../AddGroupMemberErrorDialog';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges';
|
||||
import type { SmartChooseGroupMembersModalPropsType } from '../../../state/smart/ChooseGroupMembersModal';
|
||||
import type { SmartConfirmAdditionsModalPropsType } from '../../../state/smart/ConfirmAdditionsModal';
|
||||
import {
|
||||
getGroupSizeRecommendedLimit,
|
||||
getGroupSizeHardLimit,
|
||||
|
@ -20,24 +20,24 @@ import {
|
|||
toggleSelectedContactForGroupAddition,
|
||||
OneTimeModalState,
|
||||
} from '../../../groups/toggleSelectedContactForGroupAddition';
|
||||
import { makeLookup } from '../../../util/makeLookup';
|
||||
import { deconstructLookup } from '../../../util/deconstructLookup';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
import type { RequestState } from './util';
|
||||
import { ChooseGroupMembersModal } from './AddGroupMembersModal/ChooseGroupMembersModal';
|
||||
import { ConfirmAdditionsModal } from './AddGroupMembersModal/ConfirmAdditionsModal';
|
||||
|
||||
type PropsType = {
|
||||
candidateContacts: ReadonlyArray<ConversationType>;
|
||||
clearRequestError: () => void;
|
||||
conversationIdsAlreadyInGroup: Set<string>;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
groupTitle: string;
|
||||
i18n: LocalizerType;
|
||||
makeRequest: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||
onClose: () => void;
|
||||
requestState: RequestState;
|
||||
theme: ThemeType;
|
||||
|
||||
renderChooseGroupMembersModal: (
|
||||
props: SmartChooseGroupMembersModalPropsType
|
||||
) => JSX.Element;
|
||||
renderConfirmAdditionsModal: (
|
||||
props: SmartConfirmAdditionsModalPropsType
|
||||
) => JSX.Element;
|
||||
};
|
||||
|
||||
enum Stage {
|
||||
|
@ -135,16 +135,15 @@ function reducer(
|
|||
}
|
||||
|
||||
export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||
candidateContacts,
|
||||
clearRequestError,
|
||||
conversationIdsAlreadyInGroup,
|
||||
getPreferredBadge,
|
||||
groupTitle,
|
||||
i18n,
|
||||
onClose,
|
||||
makeRequest,
|
||||
requestState,
|
||||
theme,
|
||||
renderChooseGroupMembersModal,
|
||||
renderConfirmAdditionsModal,
|
||||
}) => {
|
||||
const maxGroupSize = getMaximumNumberOfContacts();
|
||||
const maxRecommendedGroupSize = getRecommendedMaximumNumberOfContacts();
|
||||
|
@ -175,16 +174,6 @@ export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
stage: Stage.ChoosingContacts,
|
||||
});
|
||||
|
||||
const contactLookup = useMemo(
|
||||
() => makeLookup(candidateContacts, 'id'),
|
||||
[candidateContacts]
|
||||
);
|
||||
|
||||
const selectedContacts = deconstructLookup(
|
||||
contactLookup,
|
||||
selectedConversationIds
|
||||
);
|
||||
|
||||
if (maximumGroupSizeModalState === OneTimeModalState.Showing) {
|
||||
return (
|
||||
<AddGroupMemberErrorDialog
|
||||
|
@ -239,23 +228,17 @@ export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ChooseGroupMembersModal
|
||||
candidateContacts={candidateContacts}
|
||||
confirmAdds={confirmAdds}
|
||||
conversationIdsAlreadyInGroup={conversationIdsAlreadyInGroup}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
maxGroupSize={maxGroupSize}
|
||||
onClose={onClose}
|
||||
removeSelectedContact={removeSelectedContact}
|
||||
searchTerm={searchTerm}
|
||||
selectedContacts={selectedContacts}
|
||||
setSearchTerm={setSearchTerm}
|
||||
theme={theme}
|
||||
toggleSelectedContact={toggleSelectedContact}
|
||||
/>
|
||||
);
|
||||
return renderChooseGroupMembersModal({
|
||||
confirmAdds,
|
||||
selectedConversationIds,
|
||||
conversationIdsAlreadyInGroup,
|
||||
maxGroupSize,
|
||||
onClose,
|
||||
removeSelectedContact,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
toggleSelectedContact,
|
||||
});
|
||||
}
|
||||
case Stage.ConfirmingAdds: {
|
||||
const onCloseConfirmationDialog = () => {
|
||||
|
@ -263,18 +246,15 @@ export const AddGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
clearRequestError();
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmAdditionsModal
|
||||
groupTitle={groupTitle}
|
||||
i18n={i18n}
|
||||
makeRequest={() => {
|
||||
makeRequest(selectedConversationIds);
|
||||
}}
|
||||
onClose={onCloseConfirmationDialog}
|
||||
requestState={requestState}
|
||||
selectedContacts={selectedContacts}
|
||||
/>
|
||||
);
|
||||
return renderConfirmAdditionsModal({
|
||||
groupTitle,
|
||||
makeRequest: () => {
|
||||
makeRequest(selectedConversationIds);
|
||||
},
|
||||
onClose: onCloseConfirmationDialog,
|
||||
requestState,
|
||||
selectedConversationIds,
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw missingCaseError(stage);
|
||||
|
|
|
@ -2,7 +2,14 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React, { useEffect, useMemo, useState, useRef } from 'react';
|
||||
import React, {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useRef,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import type { MeasuredComponentProps } from 'react-measure';
|
||||
import Measure from 'react-measure';
|
||||
|
||||
|
@ -11,9 +18,16 @@ import { assert } from '../../../../util/assert';
|
|||
import { refMerger } from '../../../../util/refMerger';
|
||||
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
||||
import { missingCaseError } from '../../../../util/missingCaseError';
|
||||
import type { LookupConversationWithoutUuidActionsType } from '../../../../util/lookupConversationWithoutUuid';
|
||||
import { parseAndFormatPhoneNumber } from '../../../../util/libphonenumberInstance';
|
||||
import { filterAndSortConversationsByTitle } from '../../../../util/filterAndSortConversations';
|
||||
import type { ConversationType } from '../../../../state/ducks/conversations';
|
||||
import type { PreferredBadgeSelectorType } from '../../../../state/selectors/badges';
|
||||
import type {
|
||||
UUIDFetchStateKeyType,
|
||||
UUIDFetchStateType,
|
||||
} from '../../../../util/uuidFetchState';
|
||||
import { isFetchingByE164 } from '../../../../util/uuidFetchState';
|
||||
import { ModalHost } from '../../../ModalHost';
|
||||
import { ContactPills } from '../../../ContactPills';
|
||||
import { ContactPill } from '../../../ContactPill';
|
||||
|
@ -23,24 +37,37 @@ import { ContactCheckboxDisabledReason } from '../../../conversationList/Contact
|
|||
import { Button, ButtonVariant } from '../../../Button';
|
||||
import { SearchInput } from '../../../SearchInput';
|
||||
|
||||
type PropsType = {
|
||||
export type StatePropsType = {
|
||||
regionCode: string | undefined;
|
||||
candidateContacts: ReadonlyArray<ConversationType>;
|
||||
confirmAdds: () => void;
|
||||
conversationIdsAlreadyInGroup: Set<string>;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
maxGroupSize: number;
|
||||
onClose: () => void;
|
||||
removeSelectedContact: (_: string) => void;
|
||||
searchTerm: string;
|
||||
selectedContacts: ReadonlyArray<ConversationType>;
|
||||
|
||||
confirmAdds: () => void;
|
||||
onClose: () => void;
|
||||
removeSelectedContact: (_: string) => void;
|
||||
setSearchTerm: (_: string) => void;
|
||||
theme: ThemeType;
|
||||
toggleSelectedContact: (conversationId: string) => void;
|
||||
};
|
||||
} & Pick<
|
||||
LookupConversationWithoutUuidActionsType,
|
||||
'lookupConversationWithoutUuid'
|
||||
>;
|
||||
|
||||
type ActionPropsType = Omit<
|
||||
LookupConversationWithoutUuidActionsType,
|
||||
'setIsFetchingUUID' | 'lookupConversationWithoutUuid'
|
||||
>;
|
||||
|
||||
type PropsType = StatePropsType & ActionPropsType;
|
||||
|
||||
// TODO: This should use <Modal>. See DESKTOP-1038.
|
||||
export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||
regionCode,
|
||||
candidateContacts,
|
||||
confirmAdds,
|
||||
conversationIdsAlreadyInGroup,
|
||||
|
@ -54,9 +81,24 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
setSearchTerm,
|
||||
theme,
|
||||
toggleSelectedContact,
|
||||
lookupConversationWithoutUuid,
|
||||
showUserNotFoundModal,
|
||||
}) => {
|
||||
const [focusRef] = useRestoreFocus();
|
||||
|
||||
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
|
||||
|
||||
let isPhoneNumberChecked = false;
|
||||
if (phoneNumber) {
|
||||
isPhoneNumberChecked =
|
||||
phoneNumber.isValid &&
|
||||
selectedContacts.some(contact => contact.e164 === phoneNumber.e164);
|
||||
}
|
||||
|
||||
const isPhoneNumberVisible =
|
||||
phoneNumber &&
|
||||
candidateContacts.every(contact => contact.e164 !== phoneNumber.e164);
|
||||
|
||||
const inputRef = useRef<null | HTMLInputElement>(null);
|
||||
|
||||
const numberOfContactsAlreadyInGroup = conversationIdsAlreadyInGroup.size;
|
||||
|
@ -72,7 +114,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
const canContinue = Boolean(selectedContacts.length);
|
||||
|
||||
const [filteredContacts, setFilteredContacts] = useState(
|
||||
filterAndSortConversationsByTitle(candidateContacts, '')
|
||||
filterAndSortConversationsByTitle(candidateContacts, '', regionCode)
|
||||
);
|
||||
const normalizedSearchTerm = searchTerm.trim();
|
||||
useEffect(() => {
|
||||
|
@ -80,38 +122,106 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
setFilteredContacts(
|
||||
filterAndSortConversationsByTitle(
|
||||
candidateContacts,
|
||||
normalizedSearchTerm
|
||||
normalizedSearchTerm,
|
||||
regionCode
|
||||
)
|
||||
);
|
||||
}, 200);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [candidateContacts, normalizedSearchTerm, setFilteredContacts]);
|
||||
}, [
|
||||
candidateContacts,
|
||||
normalizedSearchTerm,
|
||||
setFilteredContacts,
|
||||
regionCode,
|
||||
]);
|
||||
|
||||
const rowCount = filteredContacts.length;
|
||||
const [uuidFetchState, setUuidFetchState] = useState<UUIDFetchStateType>({});
|
||||
|
||||
const setIsFetchingUUID = useCallback(
|
||||
(identifier: UUIDFetchStateKeyType, isFetching: boolean) => {
|
||||
setUuidFetchState(prevState => {
|
||||
return isFetching
|
||||
? {
|
||||
...prevState,
|
||||
[identifier]: isFetching,
|
||||
}
|
||||
: omit(prevState, identifier);
|
||||
});
|
||||
},
|
||||
[setUuidFetchState]
|
||||
);
|
||||
|
||||
let rowCount = 0;
|
||||
if (filteredContacts.length) {
|
||||
rowCount += filteredContacts.length;
|
||||
}
|
||||
if (isPhoneNumberVisible) {
|
||||
// "Contacts" header
|
||||
if (filteredContacts.length) {
|
||||
rowCount += 1;
|
||||
}
|
||||
|
||||
// "Find by phone number" + phone number
|
||||
rowCount += 2;
|
||||
}
|
||||
const getRow = (index: number): undefined | Row => {
|
||||
const contact = filteredContacts[index];
|
||||
if (!contact) {
|
||||
return undefined;
|
||||
let virtualIndex = index;
|
||||
|
||||
if (isPhoneNumberVisible && filteredContacts.length) {
|
||||
if (virtualIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'contactsHeader',
|
||||
};
|
||||
}
|
||||
|
||||
virtualIndex -= 1;
|
||||
}
|
||||
|
||||
const isSelected = selectedConversationIdsSet.has(contact.id);
|
||||
const isAlreadyInGroup = conversationIdsAlreadyInGroup.has(contact.id);
|
||||
if (virtualIndex < filteredContacts.length) {
|
||||
const contact = filteredContacts[virtualIndex];
|
||||
|
||||
let disabledReason: undefined | ContactCheckboxDisabledReason;
|
||||
if (isAlreadyInGroup) {
|
||||
disabledReason = ContactCheckboxDisabledReason.AlreadyAdded;
|
||||
} else if (hasSelectedMaximumNumberOfContacts && !isSelected) {
|
||||
disabledReason = ContactCheckboxDisabledReason.MaximumContactsSelected;
|
||||
const isSelected = selectedConversationIdsSet.has(contact.id);
|
||||
const isAlreadyInGroup = conversationIdsAlreadyInGroup.has(contact.id);
|
||||
|
||||
let disabledReason: undefined | ContactCheckboxDisabledReason;
|
||||
if (isAlreadyInGroup) {
|
||||
disabledReason = ContactCheckboxDisabledReason.AlreadyAdded;
|
||||
} else if (hasSelectedMaximumNumberOfContacts && !isSelected) {
|
||||
disabledReason = ContactCheckboxDisabledReason.MaximumContactsSelected;
|
||||
}
|
||||
|
||||
return {
|
||||
type: RowType.ContactCheckbox,
|
||||
contact,
|
||||
isChecked: isSelected || isAlreadyInGroup,
|
||||
disabledReason,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: RowType.ContactCheckbox,
|
||||
contact,
|
||||
isChecked: isSelected || isAlreadyInGroup,
|
||||
disabledReason,
|
||||
};
|
||||
virtualIndex -= filteredContacts.length;
|
||||
|
||||
if (isPhoneNumberVisible) {
|
||||
if (virtualIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'findByPhoneNumberHeader',
|
||||
};
|
||||
}
|
||||
if (virtualIndex === 1) {
|
||||
return {
|
||||
type: RowType.PhoneNumberCheckbox,
|
||||
isChecked: isPhoneNumberChecked,
|
||||
isFetching: isFetchingByE164(uuidFetchState, phoneNumber.e164),
|
||||
phoneNumber,
|
||||
};
|
||||
}
|
||||
virtualIndex -= 2;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -207,6 +317,12 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
throw missingCaseError(disabledReason);
|
||||
}
|
||||
}}
|
||||
lookupConversationWithoutUuid={
|
||||
lookupConversationWithoutUuid
|
||||
}
|
||||
showUserNotFoundModal={showUserNotFoundModal}
|
||||
setIsFetchingUUID={setIsFetchingUUID}
|
||||
showConversation={shouldNeverBeCalled}
|
||||
onSelectConversation={shouldNeverBeCalled}
|
||||
renderMessageSearchResult={() => {
|
||||
shouldNeverBeCalled();
|
||||
|
@ -215,8 +331,6 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
rowCount={rowCount}
|
||||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
startNewConversationFromPhoneNumber={shouldNeverBeCalled}
|
||||
startNewConversationFromUsername={shouldNeverBeCalled}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -15,7 +15,7 @@ import { Intl } from '../../../Intl';
|
|||
import { Emojify } from '../../Emojify';
|
||||
import { ContactName } from '../../ContactName';
|
||||
|
||||
type PropsType = {
|
||||
export type StatePropsType = {
|
||||
groupTitle: string;
|
||||
i18n: LocalizerType;
|
||||
makeRequest: () => void;
|
||||
|
@ -24,6 +24,8 @@ type PropsType = {
|
|||
selectedContacts: ReadonlyArray<ConversationType>;
|
||||
};
|
||||
|
||||
type PropsType = StatePropsType;
|
||||
|
||||
export const ConfirmAdditionsModal: FunctionComponent<PropsType> = ({
|
||||
groupTitle,
|
||||
i18n,
|
||||
|
|
|
@ -12,8 +12,11 @@ import { CapabilityError } from '../../../types/errors';
|
|||
import enMessages from '../../../../_locales/en/messages.json';
|
||||
import type { Props } from './ConversationDetails';
|
||||
import { ConversationDetails } from './ConversationDetails';
|
||||
import { ChooseGroupMembersModal } from './AddGroupMembersModal/ChooseGroupMembersModal';
|
||||
import { ConfirmAdditionsModal } from './AddGroupMembersModal/ConfirmAdditionsModal';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import { makeFakeLookupConversationWithoutUuid } from '../../../test-both/helpers/fakeLookupConversationWithoutUuid';
|
||||
import { ThemeType } from '../../../types/Util';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
@ -33,13 +36,14 @@ const conversation: ConversationType = getDefaultConversation({
|
|||
conversationColor: 'ultramarine' as const,
|
||||
});
|
||||
|
||||
const allCandidateContacts = times(10, () => getDefaultConversation());
|
||||
|
||||
const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
||||
addMembers: async () => {
|
||||
action('addMembers');
|
||||
},
|
||||
areWeASubscriber: false,
|
||||
canEditGroupInfo: false,
|
||||
candidateContactsToAdd: times(10, () => getDefaultConversation()),
|
||||
conversation: expireTimer
|
||||
? {
|
||||
...conversation,
|
||||
|
@ -97,6 +101,26 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
|||
),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
theme: ThemeType.light,
|
||||
renderChooseGroupMembersModal: props => {
|
||||
return (
|
||||
<ChooseGroupMembersModal
|
||||
{...props}
|
||||
candidateContacts={allCandidateContacts}
|
||||
selectedContacts={[]}
|
||||
regionCode="US"
|
||||
getPreferredBadge={() => undefined}
|
||||
theme={ThemeType.light}
|
||||
i18n={i18n}
|
||||
lookupConversationWithoutUuid={makeFakeLookupConversationWithoutUuid()}
|
||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||
/>
|
||||
);
|
||||
},
|
||||
renderConfirmAdditionsModal: props => {
|
||||
return (
|
||||
<ConfirmAdditionsModal {...props} selectedContacts={[]} i18n={i18n} />
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
story.add('Basic', () => {
|
||||
|
|
|
@ -8,6 +8,8 @@ import { Button, ButtonIconType, ButtonVariant } from '../../Button';
|
|||
import { Tooltip } from '../../Tooltip';
|
||||
import type { ConversationType } from '../../../state/ducks/conversations';
|
||||
import type { PreferredBadgeSelectorType } from '../../../state/selectors/badges';
|
||||
import type { SmartChooseGroupMembersModalPropsType } from '../../../state/smart/ChooseGroupMembersModal';
|
||||
import type { SmartConfirmAdditionsModalPropsType } from '../../../state/smart/ConfirmAdditionsModal';
|
||||
import { assert } from '../../../util/assert';
|
||||
import { getMutedUntilText } from '../../../util/getMutedUntilText';
|
||||
|
||||
|
@ -59,7 +61,6 @@ export type StateProps = {
|
|||
areWeASubscriber: boolean;
|
||||
badges?: ReadonlyArray<BadgeType>;
|
||||
canEditGroupInfo: boolean;
|
||||
candidateContactsToAdd: Array<ConversationType>;
|
||||
conversation?: ConversationType;
|
||||
hasGroupLink: boolean;
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
|
@ -97,6 +98,12 @@ export type StateProps = {
|
|||
setMuteExpiration: (muteExpiresAt: undefined | number) => unknown;
|
||||
onOutgoingAudioCallInConversation: () => unknown;
|
||||
onOutgoingVideoCallInConversation: () => unknown;
|
||||
renderChooseGroupMembersModal: (
|
||||
props: SmartChooseGroupMembersModalPropsType
|
||||
) => JSX.Element;
|
||||
renderConfirmAdditionsModal: (
|
||||
props: SmartConfirmAdditionsModalPropsType
|
||||
) => JSX.Element;
|
||||
};
|
||||
|
||||
type ActionProps = {
|
||||
|
@ -115,7 +122,6 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
areWeASubscriber,
|
||||
badges,
|
||||
canEditGroupInfo,
|
||||
candidateContactsToAdd,
|
||||
conversation,
|
||||
deleteAvatarFromDisk,
|
||||
hasGroupLink,
|
||||
|
@ -133,6 +139,8 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
onUnblock,
|
||||
pendingApprovalMemberships,
|
||||
pendingMemberships,
|
||||
renderChooseGroupMembersModal,
|
||||
renderConfirmAdditionsModal,
|
||||
replaceAvatar,
|
||||
saveAvatarToDisk,
|
||||
searchInConversation,
|
||||
|
@ -228,7 +236,8 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
case ModalState.AddingGroupMembers:
|
||||
modalNode = (
|
||||
<AddGroupMembersModal
|
||||
candidateContacts={candidateContactsToAdd}
|
||||
renderChooseGroupMembersModal={renderChooseGroupMembersModal}
|
||||
renderConfirmAdditionsModal={renderConfirmAdditionsModal}
|
||||
clearRequestError={() => {
|
||||
setAddGroupMembersRequestState(oldRequestState => {
|
||||
assert(
|
||||
|
@ -241,7 +250,6 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
conversationIdsAlreadyInGroup={
|
||||
new Set(memberships.map(membership => membership.member.id))
|
||||
}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
groupTitle={conversation.title}
|
||||
i18n={i18n}
|
||||
makeRequest={async conversationIds => {
|
||||
|
@ -265,7 +273,6 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
setEditGroupAttributesRequestState(RequestState.Inactive);
|
||||
}}
|
||||
requestState={addGroupMembersRequestState}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
|
|
@ -29,6 +29,7 @@ export const DATE_CLASS_NAME = `${HEADER_CLASS_NAME}__date`;
|
|||
const MESSAGE_CLASS_NAME = `${CONTENT_CLASS_NAME}__message`;
|
||||
export const MESSAGE_TEXT_CLASS_NAME = `${MESSAGE_CLASS_NAME}__text`;
|
||||
const CHECKBOX_CLASS_NAME = `${BASE_CLASS_NAME}__checkbox`;
|
||||
const SPINNER_CLASS_NAME = `${BASE_CLASS_NAME}__spinner`;
|
||||
|
||||
type PropsType = {
|
||||
checked?: boolean;
|
||||
|
@ -113,7 +114,12 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
let actionNode: ReactNode;
|
||||
if (shouldShowSpinner) {
|
||||
actionNode = (
|
||||
<Spinner size="20px" svgSize="small" direction="on-progress-dialog" />
|
||||
<Spinner
|
||||
size="20px"
|
||||
svgSize="small"
|
||||
moduleClassName={SPINNER_CLASS_NAME}
|
||||
direction="on-progress-dialog"
|
||||
/>
|
||||
);
|
||||
} else if (isCheckbox) {
|
||||
let ariaLabel: string;
|
||||
|
|
|
@ -30,6 +30,7 @@ export type ContactListItemConversationType = Pick<
|
|||
| 'title'
|
||||
| 'type'
|
||||
| 'unblurredAvatarPath'
|
||||
| 'e164'
|
||||
>;
|
||||
|
||||
type PropsDataType = ContactListItemConversationType & {
|
||||
|
|
112
ts/components/conversationList/PhoneNumberCheckbox.tsx
Normal file
112
ts/components/conversationList/PhoneNumberCheckbox.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { ButtonVariant } from '../Button';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import { AvatarColors } from '../../types/Colors';
|
||||
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
|
||||
|
||||
export type PropsDataType = {
|
||||
phoneNumber: ParsedE164Type;
|
||||
isChecked: boolean;
|
||||
isFetching: boolean;
|
||||
};
|
||||
|
||||
type PropsHousekeepingType = {
|
||||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
toggleConversationInChooseMembers: (conversationId: string) => void;
|
||||
} & LookupConversationWithoutUuidActionsType;
|
||||
|
||||
type PropsType = PropsDataType & PropsHousekeepingType;
|
||||
|
||||
export const PhoneNumberCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||
function PhoneNumberCheckbox({
|
||||
phoneNumber,
|
||||
isChecked,
|
||||
isFetching,
|
||||
theme,
|
||||
i18n,
|
||||
lookupConversationWithoutUuid,
|
||||
showUserNotFoundModal,
|
||||
setIsFetchingUUID,
|
||||
toggleConversationInChooseMembers,
|
||||
}) {
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
|
||||
const onClickItem = React.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) {
|
||||
toggleConversationInChooseMembers(conversationId);
|
||||
}
|
||||
}, [
|
||||
isFetching,
|
||||
toggleConversationInChooseMembers,
|
||||
lookupConversationWithoutUuid,
|
||||
showUserNotFoundModal,
|
||||
setIsFetchingUUID,
|
||||
setIsModalVisible,
|
||||
phoneNumber,
|
||||
]);
|
||||
|
||||
let modal: JSX.Element | undefined;
|
||||
if (isModalVisible) {
|
||||
modal = (
|
||||
<ConfirmationDialog
|
||||
cancelText={i18n('ok')}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
i18n={i18n}
|
||||
onClose={() => setIsModalVisible(false)}
|
||||
>
|
||||
{i18n('startConversation--phone-number-not-valid', {
|
||||
phoneNumber: phoneNumber.userInput,
|
||||
})}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={false}
|
||||
checked={isChecked}
|
||||
color={AvatarColors[0]}
|
||||
conversationType="direct"
|
||||
headerName={phoneNumber.userInput}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
isSelected={false}
|
||||
onClick={onClickItem}
|
||||
phoneNumber={phoneNumber.userInput}
|
||||
shouldShowSpinner={isFetching}
|
||||
theme={theme}
|
||||
sharedGroupNames={[]}
|
||||
title={phoneNumber.userInput}
|
||||
/>
|
||||
{modal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -2,54 +2,105 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
BaseConversationListItem,
|
||||
MESSAGE_TEXT_CLASS_NAME,
|
||||
} from './BaseConversationListItem';
|
||||
import { ButtonVariant } from '../Button';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
|
||||
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
||||
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { AvatarColors } from '../../types/Colors';
|
||||
|
||||
const TEXT_CLASS_NAME = `${MESSAGE_TEXT_CLASS_NAME}__start-new-conversation`;
|
||||
|
||||
type PropsData = {
|
||||
phoneNumber: string;
|
||||
phoneNumber: ParsedE164Type;
|
||||
isFetching: boolean;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
onClick: (phoneNumber: string) => void;
|
||||
};
|
||||
showConversation: (conversationId: string) => void;
|
||||
} & LookupConversationWithoutUuidActionsType;
|
||||
|
||||
export type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export const StartNewConversation: FunctionComponent<Props> = React.memo(
|
||||
function StartNewConversation({ i18n, onClick, phoneNumber }) {
|
||||
const messageText = (
|
||||
<div className={TEXT_CLASS_NAME}>{i18n('startConversation')}</div>
|
||||
);
|
||||
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 = (
|
||||
<ConfirmationDialog
|
||||
cancelText={i18n('ok')}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
i18n={i18n}
|
||||
onClose={() => setIsModalVisible(false)}
|
||||
>
|
||||
{i18n('startConversation--phone-number-not-valid', {
|
||||
phoneNumber: phoneNumber.userInput,
|
||||
})}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={false}
|
||||
color={AvatarColors[0]}
|
||||
conversationType="direct"
|
||||
headerName={phoneNumber}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
isSelected={false}
|
||||
messageText={messageText}
|
||||
onClick={boundOnClick}
|
||||
phoneNumber={phoneNumber}
|
||||
sharedGroupNames={[]}
|
||||
title={phoneNumber}
|
||||
/>
|
||||
<>
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={false}
|
||||
color={AvatarColors[0]}
|
||||
conversationType="direct"
|
||||
headerName={phoneNumber.userInput}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
isSelected={false}
|
||||
onClick={boundOnClick}
|
||||
phoneNumber={phoneNumber.userInput}
|
||||
shouldShowSpinner={isFetching}
|
||||
sharedGroupNames={[]}
|
||||
title={phoneNumber.userInput}
|
||||
/>
|
||||
{modal}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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<Props> = ({
|
||||
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 (
|
||||
<BaseConversationListItem
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// Copyright 2019-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Camelcase disabled due to emoji-datasource using snake_case
|
||||
|
@ -220,15 +220,12 @@ export function getImagePath(
|
|||
const fuse = new Fuse(data, {
|
||||
shouldSort: true,
|
||||
threshold: 0.2,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
tokenize: true,
|
||||
tokenSeparator: /[-_\s]+/,
|
||||
keys: ['short_name', 'name'],
|
||||
});
|
||||
|
||||
export function search(query: string, count = 0): Array<EmojiData> {
|
||||
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);
|
||||
|
|
|
@ -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<ConversationType>;
|
||||
isShowingRecommendedGroupSizeModal: boolean;
|
||||
isShowingMaximumGroupSizeModal: boolean;
|
||||
searchTerm: string;
|
||||
regionCode: string | undefined;
|
||||
selectedContacts: Array<ConversationType>;
|
||||
};
|
||||
|
||||
export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneChooseGroupMembersPropsType> {
|
||||
private readonly candidateContacts: ReadonlyArray<ConversationType>;
|
||||
|
||||
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<ConversationType>;
|
||||
|
||||
private readonly selectedConversationIdsSet: Set<string>;
|
||||
|
||||
private readonly uuidFetchState: UUIDFetchStateType;
|
||||
|
||||
constructor({
|
||||
candidateContacts,
|
||||
isShowingMaximumGroupSizeModal,
|
||||
isShowingRecommendedGroupSizeModal,
|
||||
searchTerm,
|
||||
regionCode,
|
||||
selectedContacts,
|
||||
uuidFetchState,
|
||||
}: Readonly<LeftPaneChooseGroupMembersPropsType>) {
|
||||
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<LeftPaneCho
|
|||
}
|
||||
|
||||
getRowCount(): number {
|
||||
if (!this.candidateContacts.length) {
|
||||
return 0;
|
||||
let rowCount = 0;
|
||||
|
||||
// Header + Phone Number
|
||||
if (this.phoneNumber) {
|
||||
rowCount += 2;
|
||||
}
|
||||
return this.candidateContacts.length + 2;
|
||||
|
||||
// Header + Contacts
|
||||
if (this.candidateContacts.length) {
|
||||
rowCount += 1 + this.candidateContacts.length;
|
||||
}
|
||||
|
||||
// Footer
|
||||
if (rowCount > 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
|
||||
|
|
|
@ -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<ContactListItemConversationType>;
|
||||
|
@ -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<LeftPaneComposePropsType> {
|
||||
|
@ -41,13 +41,15 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
|
||||
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
||||
|
||||
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<LeftPaneComposePropsTy
|
|||
regionCode,
|
||||
searchTerm,
|
||||
isUsernamesEnabled,
|
||||
isFetchingUsername,
|
||||
uuidFetchState,
|
||||
}: Readonly<LeftPaneComposePropsType>) {
|
||||
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<LeftPaneComposePropsTy
|
|||
if (this.getUsernameFromSearch()) {
|
||||
result += 2;
|
||||
}
|
||||
if (this.isPhoneNumberVisible) {
|
||||
result += 2;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -153,18 +166,6 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
switch (topButton) {
|
||||
case TopButton.None:
|
||||
break;
|
||||
case TopButton.StartNewConversation:
|
||||
assert(
|
||||
this.phoneNumber,
|
||||
'LeftPaneComposeHelper: we should have a phone number if the top button is "Start new conversation"'
|
||||
);
|
||||
return {
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: phoneNumberInstance.format(
|
||||
this.phoneNumber,
|
||||
PhoneNumberFormat.E164
|
||||
),
|
||||
};
|
||||
case TopButton.CreateNewGroup:
|
||||
return { type: RowType.CreateNewGroup };
|
||||
default:
|
||||
|
@ -232,7 +233,34 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
return {
|
||||
type: RowType.UsernameSearchResult,
|
||||
username,
|
||||
isFetchingUsername: this.isFetchingUsername,
|
||||
isFetchingUsername: isFetchingByUsername(
|
||||
this.uuidFetchState,
|
||||
username
|
||||
),
|
||||
};
|
||||
|
||||
virtualRowIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.phoneNumber && this.isPhoneNumberVisible) {
|
||||
if (virtualRowIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'findByPhoneNumberHeader',
|
||||
};
|
||||
}
|
||||
|
||||
virtualRowIndex -= 1;
|
||||
|
||||
if (virtualRowIndex === 0) {
|
||||
return {
|
||||
type: RowType.StartNewConversation,
|
||||
phoneNumber: this.phoneNumber,
|
||||
isFetching: isFetchingByE164(
|
||||
this.uuidFetchState,
|
||||
this.phoneNumber.e164
|
||||
),
|
||||
};
|
||||
|
||||
virtualRowIndex -= 1;
|
||||
|
@ -272,9 +300,6 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
}
|
||||
|
||||
private getTopButton(): TopButton {
|
||||
if (this.phoneNumber) {
|
||||
return TopButton.StartNewConversation;
|
||||
}
|
||||
if (this.searchTerm) {
|
||||
return TopButton.None;
|
||||
}
|
||||
|
@ -352,21 +377,3 @@ function focusRef(el: HTMLElement | null) {
|
|||
el.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function parsePhoneNumber(
|
||||
str: string,
|
||||
regionCode: string | undefined
|
||||
): undefined | PhoneNumber {
|
||||
let result: PhoneNumber;
|
||||
try {
|
||||
result = phoneNumberInstance.parse(str, regionCode);
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!phoneNumberInstance.isValidNumber(result)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// Copyright 2020-2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import Fuse from 'fuse.js';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { getOwn } from '../util/getOwn';
|
||||
import { filter, map } from '../util/iterables';
|
||||
|
||||
const FUSE_OPTIONS = {
|
||||
|
@ -16,10 +16,10 @@ const FUSE_OPTIONS = {
|
|||
keys: ['name', 'firstName', 'profileName', 'title'],
|
||||
getFn(
|
||||
conversation: Readonly<ConversationType>,
|
||||
path: string
|
||||
path: string | Array<string>
|
||||
): ReadonlyArray<string> | string {
|
||||
// It'd be nice to avoid this cast, but Fuse's types don't allow it.
|
||||
const rawValue = getOwn(conversation as Record<string, unknown>, path);
|
||||
const rawValue = get(conversation as Record<string, unknown>, 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);
|
||||
|
|
|
@ -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<void, RootStateType, unknown, ShowInboxActionType> {
|
||||
return dispatch => {
|
||||
trigger('showConversation', e164);
|
||||
|
||||
dispatch(showInbox());
|
||||
};
|
||||
}
|
||||
|
||||
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: 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<void, RootStateType, unknown, ShowInboxActionType> {
|
||||
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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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<ConversationType>
|
||||
contacts: Array<ConversationType>,
|
||||
regionCode: string | undefined
|
||||
): Array<ConversationType> => {
|
||||
return filterAndSortConversationsByTitle(contacts, searchTerm);
|
||||
return filterAndSortConversationsByTitle(contacts, searchTerm, regionCode);
|
||||
}
|
||||
);
|
||||
|
||||
export const getFilteredComposeGroups = createSelector(
|
||||
getNormalizedComposerConversationSearchTerm,
|
||||
getComposableGroups,
|
||||
getRegionCode,
|
||||
(
|
||||
searchTerm: string,
|
||||
groups: Array<ConversationType>
|
||||
groups: Array<ConversationType>,
|
||||
regionCode: string | undefined
|
||||
): Array<ConversationType> => {
|
||||
return filterAndSortConversationsByTitle(groups, searchTerm);
|
||||
return filterAndSortConversationsByTitle(groups, searchTerm, regionCode);
|
||||
}
|
||||
);
|
||||
|
||||
export const getFilteredCandidateContactsForNewGroup = createSelector(
|
||||
getCandidateContactsForNewGroup,
|
||||
getNormalizedComposerConversationSearchTerm,
|
||||
getRegionCode,
|
||||
filterAndSortConversationsByTitle
|
||||
);
|
||||
|
||||
|
|
63
ts/state/smart/ChooseGroupMembersModal.tsx
Normal file
63
ts/state/smart/ChooseGroupMembersModal.tsx
Normal file
|
@ -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<string>;
|
||||
maxGroupSize: number;
|
||||
confirmAdds: () => void;
|
||||
onClose: () => void;
|
||||
removeSelectedContact: (_: string) => void;
|
||||
searchTerm: string;
|
||||
selectedConversationIds: ReadonlyArray<string>;
|
||||
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,
|
||||
'<SmartChooseGroupMemberModal> 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);
|
49
ts/state/smart/ConfirmAdditionsModal.tsx
Normal file
49
ts/state/smart/ConfirmAdditionsModal.tsx
Normal file
|
@ -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<string>;
|
||||
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,
|
||||
'<SmartChooseGroupMemberModal> selected conversation not found'
|
||||
);
|
||||
return convo;
|
||||
});
|
||||
|
||||
return {
|
||||
...props,
|
||||
selectedContacts,
|
||||
i18n: getIntl(state),
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartConfirmAdditionsModal = smart(ConfirmAdditionsModal);
|
|
@ -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<string>) => Promise<void>;
|
||||
|
@ -56,6 +60,18 @@ export type SmartConversationDetailsProps = {
|
|||
|
||||
const ACCESS_ENUM = Proto.AccessControl.AccessRequired;
|
||||
|
||||
const renderChooseGroupMembersModal = (
|
||||
props: SmartChooseGroupMembersModalPropsType
|
||||
) => {
|
||||
return <SmartChooseGroupMembersModal {...props} />;
|
||||
};
|
||||
|
||||
const renderConfirmAdditionsModal = (
|
||||
props: SmartConfirmAdditionsModalPropsType
|
||||
) => {
|
||||
return <SmartConfirmAdditionsModal {...props} />;
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
103
ts/test-both/helpers/fakeLookupConversationWithoutUuid.ts
Normal file
103
ts/test-both/helpers/fakeLookupConversationWithoutUuid.ts
Normal file
|
@ -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<UUIDFetchStateKeyType>([
|
||||
'e164:+12125551234',
|
||||
'username:bobross',
|
||||
]);
|
||||
|
||||
export function makeFakeLookupConversationWithoutUuid(
|
||||
saveConversation?: (convo: ConversationType) => void
|
||||
): typeof lookupConversationWithoutUuid {
|
||||
const cache = new Map<UUIDFetchStateKeyType, ConversationType>();
|
||||
|
||||
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];
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: [],
|
||||
};
|
||||
|
||||
|
|
|
@ -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: {},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}`]);
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue