Add to group by username
This commit is contained in:
parent
8dd321d0b6
commit
973b2264fe
13 changed files with 332 additions and 45 deletions
|
@ -25,6 +25,7 @@ import { ContactListItem } from './conversationList/ContactListItem';
|
||||||
import type { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
|
import type { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
|
||||||
import { ContactCheckbox as ContactCheckboxComponent } from './conversationList/ContactCheckbox';
|
import { ContactCheckbox as ContactCheckboxComponent } from './conversationList/ContactCheckbox';
|
||||||
import { PhoneNumberCheckbox as PhoneNumberCheckboxComponent } from './conversationList/PhoneNumberCheckbox';
|
import { PhoneNumberCheckbox as PhoneNumberCheckboxComponent } from './conversationList/PhoneNumberCheckbox';
|
||||||
|
import { UsernameCheckbox as UsernameCheckboxComponent } from './conversationList/UsernameCheckbox';
|
||||||
import { CreateNewGroupButton } from './conversationList/CreateNewGroupButton';
|
import { CreateNewGroupButton } from './conversationList/CreateNewGroupButton';
|
||||||
import { StartNewConversation as StartNewConversationComponent } from './conversationList/StartNewConversation';
|
import { StartNewConversation as StartNewConversationComponent } from './conversationList/StartNewConversation';
|
||||||
import { SearchResultsLoadingFakeHeader as SearchResultsLoadingFakeHeaderComponent } from './conversationList/SearchResultsLoadingFakeHeader';
|
import { SearchResultsLoadingFakeHeader as SearchResultsLoadingFakeHeaderComponent } from './conversationList/SearchResultsLoadingFakeHeader';
|
||||||
|
@ -32,19 +33,20 @@ import { SearchResultsLoadingFakeRow as SearchResultsLoadingFakeRowComponent } f
|
||||||
import { UsernameSearchResultListItem } from './conversationList/UsernameSearchResultListItem';
|
import { UsernameSearchResultListItem } from './conversationList/UsernameSearchResultListItem';
|
||||||
|
|
||||||
export enum RowType {
|
export enum RowType {
|
||||||
ArchiveButton,
|
ArchiveButton = 'ArchiveButton',
|
||||||
Blank,
|
Blank = 'Blank',
|
||||||
Contact,
|
Contact = 'Contact',
|
||||||
ContactCheckbox,
|
ContactCheckbox = 'ContactCheckbox',
|
||||||
PhoneNumberCheckbox,
|
PhoneNumberCheckbox = 'PhoneNumberCheckbox',
|
||||||
Conversation,
|
UsernameCheckbox = 'UsernameCheckbox',
|
||||||
CreateNewGroup,
|
Conversation = 'Conversation',
|
||||||
Header,
|
CreateNewGroup = 'CreateNewGroup',
|
||||||
MessageSearchResult,
|
Header = 'Header',
|
||||||
SearchResultsLoadingFakeHeader,
|
MessageSearchResult = 'MessageSearchResult',
|
||||||
SearchResultsLoadingFakeRow,
|
SearchResultsLoadingFakeHeader = 'SearchResultsLoadingFakeHeader',
|
||||||
StartNewConversation,
|
SearchResultsLoadingFakeRow = 'SearchResultsLoadingFakeRow',
|
||||||
UsernameSearchResult,
|
StartNewConversation = 'StartNewConversation',
|
||||||
|
UsernameSearchResult = 'UsernameSearchResult',
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchiveButtonRowType = {
|
type ArchiveButtonRowType = {
|
||||||
|
@ -74,6 +76,13 @@ type PhoneNumberCheckboxRowType = {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UsernameCheckboxRowType = {
|
||||||
|
type: RowType.UsernameCheckbox;
|
||||||
|
username: string;
|
||||||
|
isChecked: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type ConversationRowType = {
|
type ConversationRowType = {
|
||||||
type: RowType.Conversation;
|
type: RowType.Conversation;
|
||||||
conversation: ConversationListItemPropsType;
|
conversation: ConversationListItemPropsType;
|
||||||
|
@ -119,6 +128,7 @@ export type Row =
|
||||||
| ContactRowType
|
| ContactRowType
|
||||||
| ContactCheckboxRowType
|
| ContactCheckboxRowType
|
||||||
| PhoneNumberCheckboxRowType
|
| PhoneNumberCheckboxRowType
|
||||||
|
| UsernameCheckboxRowType
|
||||||
| ConversationRowType
|
| ConversationRowType
|
||||||
| CreateNewGroupRowType
|
| CreateNewGroupRowType
|
||||||
| MessageRowType
|
| MessageRowType
|
||||||
|
@ -283,6 +293,23 @@ export const ConversationList: React.FC<PropsType> = ({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case RowType.UsernameCheckbox:
|
||||||
|
result = (
|
||||||
|
<UsernameCheckboxComponent
|
||||||
|
username={row.username}
|
||||||
|
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: {
|
case RowType.Conversation: {
|
||||||
const itemProps = pick(row.conversation, [
|
const itemProps = pick(row.conversation, [
|
||||||
'acceptedMessageRequest',
|
'acceptedMessageRequest',
|
||||||
|
|
|
@ -883,6 +883,7 @@ export const ChooseGroupMembersPartialPhoneNumber = (): JSX.Element => (
|
||||||
candidateContacts: [],
|
candidateContacts: [],
|
||||||
isShowingRecommendedGroupSizeModal: false,
|
isShowingRecommendedGroupSizeModal: false,
|
||||||
isShowingMaximumGroupSizeModal: false,
|
isShowingMaximumGroupSizeModal: false,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
searchTerm: '+1(212) 555',
|
searchTerm: '+1(212) 555',
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
selectedContacts: [],
|
selectedContacts: [],
|
||||||
|
@ -904,6 +905,7 @@ export const ChooseGroupMembersValidPhoneNumber = (): JSX.Element => (
|
||||||
candidateContacts: [],
|
candidateContacts: [],
|
||||||
isShowingRecommendedGroupSizeModal: false,
|
isShowingRecommendedGroupSizeModal: false,
|
||||||
isShowingMaximumGroupSizeModal: false,
|
isShowingMaximumGroupSizeModal: false,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
searchTerm: '+1(212) 555 5454',
|
searchTerm: '+1(212) 555 5454',
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
selectedContacts: [],
|
selectedContacts: [],
|
||||||
|
@ -916,6 +918,28 @@ ChooseGroupMembersValidPhoneNumber.story = {
|
||||||
name: 'Choose Group Members: Valid phone number',
|
name: 'Choose Group Members: Valid phone number',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ChooseGroupMembersUsername = (): JSX.Element => (
|
||||||
|
<LeftPane
|
||||||
|
{...useProps({
|
||||||
|
modeSpecificProps: {
|
||||||
|
mode: LeftPaneMode.ChooseGroupMembers,
|
||||||
|
uuidFetchState: {},
|
||||||
|
candidateContacts: [],
|
||||||
|
isShowingRecommendedGroupSizeModal: false,
|
||||||
|
isShowingMaximumGroupSizeModal: false,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
searchTerm: '@signal',
|
||||||
|
regionCode: 'US',
|
||||||
|
selectedContacts: [],
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
ChooseGroupMembersUsername.story = {
|
||||||
|
name: 'Choose Group Members: username',
|
||||||
|
};
|
||||||
|
|
||||||
export const GroupMetadataNoTimer = (): JSX.Element => (
|
export const GroupMetadataNoTimer = (): JSX.Element => (
|
||||||
<LeftPane
|
<LeftPane
|
||||||
{...useProps({
|
{...useProps({
|
||||||
|
|
|
@ -68,6 +68,7 @@ const createProps = (
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
|
lookupConversationWithoutUuid={lookupConversationWithoutUuid}
|
||||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||||
|
isUsernamesEnabled
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,6 +14,7 @@ import type { MeasuredComponentProps } from 'react-measure';
|
||||||
import Measure from 'react-measure';
|
import Measure from 'react-measure';
|
||||||
|
|
||||||
import type { LocalizerType, ThemeType } from '../../../../types/Util';
|
import type { LocalizerType, ThemeType } from '../../../../types/Util';
|
||||||
|
import { getUsernameFromSearch } from '../../../../types/Username';
|
||||||
import { assert } from '../../../../util/assert';
|
import { assert } from '../../../../util/assert';
|
||||||
import { refMerger } from '../../../../util/refMerger';
|
import { refMerger } from '../../../../util/refMerger';
|
||||||
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
||||||
|
@ -27,7 +28,10 @@ import type {
|
||||||
UUIDFetchStateKeyType,
|
UUIDFetchStateKeyType,
|
||||||
UUIDFetchStateType,
|
UUIDFetchStateType,
|
||||||
} from '../../../../util/uuidFetchState';
|
} from '../../../../util/uuidFetchState';
|
||||||
import { isFetchingByE164 } from '../../../../util/uuidFetchState';
|
import {
|
||||||
|
isFetchingByE164,
|
||||||
|
isFetchingByUsername,
|
||||||
|
} from '../../../../util/uuidFetchState';
|
||||||
import { ModalHost } from '../../../ModalHost';
|
import { ModalHost } from '../../../ModalHost';
|
||||||
import { ContactPills } from '../../../ContactPills';
|
import { ContactPills } from '../../../ContactPills';
|
||||||
import { ContactPill } from '../../../ContactPill';
|
import { ContactPill } from '../../../ContactPill';
|
||||||
|
@ -53,6 +57,7 @@ export type StatePropsType = {
|
||||||
removeSelectedContact: (_: string) => void;
|
removeSelectedContact: (_: string) => void;
|
||||||
setSearchTerm: (_: string) => void;
|
setSearchTerm: (_: string) => void;
|
||||||
toggleSelectedContact: (conversationId: string) => void;
|
toggleSelectedContact: (conversationId: string) => void;
|
||||||
|
isUsernamesEnabled: boolean;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
LookupConversationWithoutUuidActionsType,
|
LookupConversationWithoutUuidActionsType,
|
||||||
'lookupConversationWithoutUuid'
|
'lookupConversationWithoutUuid'
|
||||||
|
@ -83,6 +88,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||||
toggleSelectedContact,
|
toggleSelectedContact,
|
||||||
lookupConversationWithoutUuid,
|
lookupConversationWithoutUuid,
|
||||||
showUserNotFoundModal,
|
showUserNotFoundModal,
|
||||||
|
isUsernamesEnabled,
|
||||||
}) => {
|
}) => {
|
||||||
const [focusRef] = useRestoreFocus();
|
const [focusRef] = useRestoreFocus();
|
||||||
|
|
||||||
|
@ -99,6 +105,21 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||||
phoneNumber &&
|
phoneNumber &&
|
||||||
candidateContacts.every(contact => contact.e164 !== phoneNumber.e164);
|
candidateContacts.every(contact => contact.e164 !== phoneNumber.e164);
|
||||||
|
|
||||||
|
let username: string | undefined;
|
||||||
|
let isUsernameChecked = false;
|
||||||
|
let isUsernameVisible = false;
|
||||||
|
if (!phoneNumber && isUsernamesEnabled) {
|
||||||
|
username = getUsernameFromSearch(searchTerm);
|
||||||
|
|
||||||
|
isUsernameChecked = selectedContacts.some(
|
||||||
|
contact => contact.username === username
|
||||||
|
);
|
||||||
|
|
||||||
|
isUsernameVisible = candidateContacts.every(
|
||||||
|
contact => contact.username !== username
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const inputRef = useRef<null | HTMLInputElement>(null);
|
const inputRef = useRef<null | HTMLInputElement>(null);
|
||||||
|
|
||||||
const numberOfContactsAlreadyInGroup = conversationIdsAlreadyInGroup.size;
|
const numberOfContactsAlreadyInGroup = conversationIdsAlreadyInGroup.size;
|
||||||
|
@ -157,19 +178,24 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||||
if (filteredContacts.length) {
|
if (filteredContacts.length) {
|
||||||
rowCount += filteredContacts.length;
|
rowCount += filteredContacts.length;
|
||||||
}
|
}
|
||||||
if (isPhoneNumberVisible) {
|
if (isPhoneNumberVisible || isUsernameVisible) {
|
||||||
// "Contacts" header
|
// "Contacts" header
|
||||||
if (filteredContacts.length) {
|
if (filteredContacts.length) {
|
||||||
rowCount += 1;
|
rowCount += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Find by phone number" + phone number
|
// "Find by phone number" + phone number
|
||||||
|
// or
|
||||||
|
// "Find by username" + username
|
||||||
rowCount += 2;
|
rowCount += 2;
|
||||||
}
|
}
|
||||||
const getRow = (index: number): undefined | Row => {
|
const getRow = (index: number): undefined | Row => {
|
||||||
let virtualIndex = index;
|
let virtualIndex = index;
|
||||||
|
|
||||||
if (isPhoneNumberVisible && filteredContacts.length) {
|
if (
|
||||||
|
(isPhoneNumberVisible || isUsernameVisible) &&
|
||||||
|
filteredContacts.length
|
||||||
|
) {
|
||||||
if (virtualIndex === 0) {
|
if (virtualIndex === 0) {
|
||||||
return {
|
return {
|
||||||
type: RowType.Header,
|
type: RowType.Header,
|
||||||
|
@ -221,6 +247,24 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||||
virtualIndex -= 2;
|
virtualIndex -= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (username) {
|
||||||
|
if (virtualIndex === 0) {
|
||||||
|
return {
|
||||||
|
type: RowType.Header,
|
||||||
|
i18nKey: 'findByUsernameHeader',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (virtualIndex === 1) {
|
||||||
|
return {
|
||||||
|
type: RowType.UsernameCheckbox,
|
||||||
|
isChecked: isUsernameChecked,
|
||||||
|
isFetching: isFetchingByUsername(uuidFetchState, username),
|
||||||
|
username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
virtualIndex -= 2;
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -110,6 +110,7 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
lookupConversationWithoutUuid={makeFakeLookupConversationWithoutUuid()}
|
lookupConversationWithoutUuid={makeFakeLookupConversationWithoutUuid()}
|
||||||
showUserNotFoundModal={action('showUserNotFoundModal')}
|
showUserNotFoundModal={action('showUserNotFoundModal')}
|
||||||
|
isUsernamesEnabled
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,6 +30,7 @@ export type ContactListItemConversationType = Pick<
|
||||||
| 'title'
|
| 'title'
|
||||||
| 'type'
|
| 'type'
|
||||||
| 'unblurredAvatarPath'
|
| 'unblurredAvatarPath'
|
||||||
|
| 'username'
|
||||||
| 'e164'
|
| 'e164'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
|
84
ts/components/conversationList/UsernameCheckbox.tsx
Normal file
84
ts/components/conversationList/UsernameCheckbox.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2022 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { FunctionComponent } from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||||
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
|
import { AvatarColors } from '../../types/Colors';
|
||||||
|
import type { LookupConversationWithoutUuidActionsType } from '../../util/lookupConversationWithoutUuid';
|
||||||
|
|
||||||
|
export type PropsDataType = {
|
||||||
|
username: string;
|
||||||
|
isChecked: boolean;
|
||||||
|
isFetching: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PropsHousekeepingType = {
|
||||||
|
i18n: LocalizerType;
|
||||||
|
theme: ThemeType;
|
||||||
|
toggleConversationInChooseMembers: (conversationId: string) => void;
|
||||||
|
} & LookupConversationWithoutUuidActionsType;
|
||||||
|
|
||||||
|
type PropsType = PropsDataType & PropsHousekeepingType;
|
||||||
|
|
||||||
|
export const UsernameCheckbox: FunctionComponent<PropsType> = React.memo(
|
||||||
|
function UsernameCheckbox({
|
||||||
|
username,
|
||||||
|
isChecked,
|
||||||
|
isFetching,
|
||||||
|
theme,
|
||||||
|
i18n,
|
||||||
|
lookupConversationWithoutUuid,
|
||||||
|
showUserNotFoundModal,
|
||||||
|
setIsFetchingUUID,
|
||||||
|
toggleConversationInChooseMembers,
|
||||||
|
}) {
|
||||||
|
const onClickItem = React.useCallback(async () => {
|
||||||
|
if (isFetching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversationId = await lookupConversationWithoutUuid({
|
||||||
|
showUserNotFoundModal,
|
||||||
|
setIsFetchingUUID,
|
||||||
|
|
||||||
|
type: 'username',
|
||||||
|
username,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conversationId !== undefined) {
|
||||||
|
toggleConversationInChooseMembers(conversationId);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isFetching,
|
||||||
|
toggleConversationInChooseMembers,
|
||||||
|
lookupConversationWithoutUuid,
|
||||||
|
showUserNotFoundModal,
|
||||||
|
setIsFetchingUUID,
|
||||||
|
username,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const title = i18n('at-username', { username });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseConversationListItem
|
||||||
|
acceptedMessageRequest={false}
|
||||||
|
checked={isChecked}
|
||||||
|
color={AvatarColors[0]}
|
||||||
|
conversationType="direct"
|
||||||
|
headerName={title}
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={false}
|
||||||
|
isSelected={false}
|
||||||
|
isUsernameSearchResult
|
||||||
|
onClick={onClickItem}
|
||||||
|
shouldShowSpinner={isFetching}
|
||||||
|
theme={theme}
|
||||||
|
sharedGroupNames={[]}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -18,10 +18,14 @@ import {
|
||||||
} from '../AddGroupMemberErrorDialog';
|
} from '../AddGroupMemberErrorDialog';
|
||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import { getUsernameFromSearch } from '../../types/Username';
|
||||||
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
import type { ParsedE164Type } from '../../util/libphonenumberInstance';
|
||||||
import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
|
import { parseAndFormatPhoneNumber } from '../../util/libphonenumberInstance';
|
||||||
import type { UUIDFetchStateType } from '../../util/uuidFetchState';
|
import type { UUIDFetchStateType } from '../../util/uuidFetchState';
|
||||||
import { isFetchingByE164 } from '../../util/uuidFetchState';
|
import {
|
||||||
|
isFetchingByUsername,
|
||||||
|
isFetchingByE164,
|
||||||
|
} from '../../util/uuidFetchState';
|
||||||
import {
|
import {
|
||||||
getGroupSizeRecommendedLimit,
|
getGroupSizeRecommendedLimit,
|
||||||
getGroupSizeHardLimit,
|
getGroupSizeHardLimit,
|
||||||
|
@ -32,6 +36,7 @@ export type LeftPaneChooseGroupMembersPropsType = {
|
||||||
candidateContacts: ReadonlyArray<ConversationType>;
|
candidateContacts: ReadonlyArray<ConversationType>;
|
||||||
isShowingRecommendedGroupSizeModal: boolean;
|
isShowingRecommendedGroupSizeModal: boolean;
|
||||||
isShowingMaximumGroupSizeModal: boolean;
|
isShowingMaximumGroupSizeModal: boolean;
|
||||||
|
isUsernamesEnabled: boolean;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
regionCode: string | undefined;
|
regionCode: string | undefined;
|
||||||
selectedContacts: Array<ConversationType>;
|
selectedContacts: Array<ConversationType>;
|
||||||
|
@ -42,6 +47,8 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
|
|
||||||
private readonly isPhoneNumberChecked: boolean;
|
private readonly isPhoneNumberChecked: boolean;
|
||||||
|
|
||||||
|
private readonly isUsernameChecked: boolean;
|
||||||
|
|
||||||
private readonly isShowingMaximumGroupSizeModal: boolean;
|
private readonly isShowingMaximumGroupSizeModal: boolean;
|
||||||
|
|
||||||
private readonly isShowingRecommendedGroupSizeModal: boolean;
|
private readonly isShowingRecommendedGroupSizeModal: boolean;
|
||||||
|
@ -50,6 +57,8 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
|
|
||||||
private readonly phoneNumber: ParsedE164Type | undefined;
|
private readonly phoneNumber: ParsedE164Type | undefined;
|
||||||
|
|
||||||
|
private readonly username: string | undefined;
|
||||||
|
|
||||||
private readonly selectedContacts: Array<ConversationType>;
|
private readonly selectedContacts: Array<ConversationType>;
|
||||||
|
|
||||||
private readonly selectedConversationIdsSet: Set<string>;
|
private readonly selectedConversationIdsSet: Set<string>;
|
||||||
|
@ -60,6 +69,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
candidateContacts,
|
candidateContacts,
|
||||||
isShowingMaximumGroupSizeModal,
|
isShowingMaximumGroupSizeModal,
|
||||||
isShowingRecommendedGroupSizeModal,
|
isShowingRecommendedGroupSizeModal,
|
||||||
|
isUsernamesEnabled,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
regionCode,
|
regionCode,
|
||||||
selectedContacts,
|
selectedContacts,
|
||||||
|
@ -90,6 +100,22 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
} else {
|
} else {
|
||||||
this.isPhoneNumberChecked = false;
|
this.isPhoneNumberChecked = false;
|
||||||
}
|
}
|
||||||
|
if (!this.phoneNumber && isUsernamesEnabled) {
|
||||||
|
const username = getUsernameFromSearch(searchTerm);
|
||||||
|
const isVisible = this.candidateContacts.every(
|
||||||
|
contact => contact.username !== username
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isUsernameChecked = selectedContacts.some(
|
||||||
|
contact => contact.username === this.username
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.isUsernameChecked = false;
|
||||||
|
}
|
||||||
this.selectedContacts = selectedContacts;
|
this.selectedContacts = selectedContacts;
|
||||||
|
|
||||||
this.selectedConversationIdsSet = new Set(
|
this.selectedConversationIdsSet = new Set(
|
||||||
|
@ -246,6 +272,11 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
rowCount += 2;
|
rowCount += 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Header + Username
|
||||||
|
if (this.username) {
|
||||||
|
rowCount += 2;
|
||||||
|
}
|
||||||
|
|
||||||
// Header + Contacts
|
// Header + Contacts
|
||||||
if (this.candidateContacts.length) {
|
if (this.candidateContacts.length) {
|
||||||
rowCount += 1 + this.candidateContacts.length;
|
rowCount += 1 + this.candidateContacts.length;
|
||||||
|
@ -260,7 +291,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
}
|
}
|
||||||
|
|
||||||
getRow(actualRowIndex: number): undefined | Row {
|
getRow(actualRowIndex: number): undefined | Row {
|
||||||
if (!this.candidateContacts.length && !this.phoneNumber) {
|
if (!this.candidateContacts.length && !this.phoneNumber && !this.username) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,6 +353,24 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
|
||||||
virtualRowIndex -= 2;
|
virtualRowIndex -= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.username) {
|
||||||
|
if (virtualRowIndex === 0) {
|
||||||
|
return {
|
||||||
|
type: RowType.Header,
|
||||||
|
i18nKey: 'findByUsernameHeader',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (virtualRowIndex === 1) {
|
||||||
|
return {
|
||||||
|
type: RowType.UsernameCheckbox,
|
||||||
|
isChecked: this.isUsernameChecked,
|
||||||
|
isFetching: isFetchingByUsername(this.uuidFetchState, this.username),
|
||||||
|
username: this.username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
virtualRowIndex -= 2;
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,14 +43,16 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
|
|
||||||
private readonly uuidFetchState: UUIDFetchStateType;
|
private readonly uuidFetchState: UUIDFetchStateType;
|
||||||
|
|
||||||
private readonly isUsernamesEnabled: boolean;
|
|
||||||
|
|
||||||
private readonly searchTerm: string;
|
private readonly searchTerm: string;
|
||||||
|
|
||||||
private readonly phoneNumber: ParsedE164Type | undefined;
|
private readonly phoneNumber: ParsedE164Type | undefined;
|
||||||
|
|
||||||
private readonly isPhoneNumberVisible: boolean;
|
private readonly isPhoneNumberVisible: boolean;
|
||||||
|
|
||||||
|
private readonly username: string | undefined;
|
||||||
|
|
||||||
|
private readonly isUsernameVisible: boolean;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
composeContacts,
|
composeContacts,
|
||||||
composeGroups,
|
composeGroups,
|
||||||
|
@ -74,7 +76,18 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
this.isPhoneNumberVisible = false;
|
this.isPhoneNumberVisible = false;
|
||||||
}
|
}
|
||||||
this.uuidFetchState = uuidFetchState;
|
this.uuidFetchState = uuidFetchState;
|
||||||
this.isUsernamesEnabled = isUsernamesEnabled;
|
|
||||||
|
if (isUsernamesEnabled && !this.phoneNumber) {
|
||||||
|
this.username = getUsernameFromSearch(this.searchTerm);
|
||||||
|
this.isUsernameVisible =
|
||||||
|
isUsernamesEnabled &&
|
||||||
|
Boolean(this.username) &&
|
||||||
|
this.composeContacts.every(
|
||||||
|
contact => contact.username !== this.username
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.isUsernameVisible = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override getHeaderContents({
|
override getHeaderContents({
|
||||||
|
@ -148,7 +161,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
if (this.hasGroupsHeader()) {
|
if (this.hasGroupsHeader()) {
|
||||||
result += 1;
|
result += 1;
|
||||||
}
|
}
|
||||||
if (this.getUsernameFromSearch()) {
|
if (this.isUsernameVisible) {
|
||||||
result += 2;
|
result += 2;
|
||||||
}
|
}
|
||||||
if (this.isPhoneNumberVisible) {
|
if (this.isPhoneNumberVisible) {
|
||||||
|
@ -218,8 +231,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
virtualRowIndex -= this.composeGroups.length;
|
virtualRowIndex -= this.composeGroups.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const username = this.getUsernameFromSearch();
|
if (this.username && this.isUsernameVisible) {
|
||||||
if (username) {
|
|
||||||
if (virtualRowIndex === 0) {
|
if (virtualRowIndex === 0) {
|
||||||
return {
|
return {
|
||||||
type: RowType.Header,
|
type: RowType.Header,
|
||||||
|
@ -232,10 +244,10 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
if (virtualRowIndex === 0) {
|
if (virtualRowIndex === 0) {
|
||||||
return {
|
return {
|
||||||
type: RowType.UsernameSearchResult,
|
type: RowType.UsernameSearchResult,
|
||||||
username,
|
username: this.username,
|
||||||
isFetchingUsername: isFetchingByUsername(
|
isFetchingUsername: isFetchingByUsername(
|
||||||
this.uuidFetchState,
|
this.uuidFetchState,
|
||||||
username
|
this.username
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -295,7 +307,8 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
currHeaderIndices.top !== prevHeaderIndices.top ||
|
currHeaderIndices.top !== prevHeaderIndices.top ||
|
||||||
currHeaderIndices.contact !== prevHeaderIndices.contact ||
|
currHeaderIndices.contact !== prevHeaderIndices.contact ||
|
||||||
currHeaderIndices.group !== prevHeaderIndices.group ||
|
currHeaderIndices.group !== prevHeaderIndices.group ||
|
||||||
currHeaderIndices.username !== prevHeaderIndices.username
|
currHeaderIndices.username !== prevHeaderIndices.username ||
|
||||||
|
currHeaderIndices.phoneNumber !== prevHeaderIndices.phoneNumber
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,31 +331,17 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
return Boolean(this.composeGroups.length);
|
return Boolean(this.composeGroups.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getUsernameFromSearch(): string | undefined {
|
|
||||||
if (!this.isUsernamesEnabled) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.phoneNumber) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.searchTerm) {
|
|
||||||
return getUsernameFromSearch(this.searchTerm);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getHeaderIndices(): {
|
private getHeaderIndices(): {
|
||||||
top?: number;
|
top?: number;
|
||||||
contact?: number;
|
contact?: number;
|
||||||
group?: number;
|
group?: number;
|
||||||
|
phoneNumber?: number;
|
||||||
username?: number;
|
username?: number;
|
||||||
} {
|
} {
|
||||||
let top: number | undefined;
|
let top: number | undefined;
|
||||||
let contact: number | undefined;
|
let contact: number | undefined;
|
||||||
let group: number | undefined;
|
let group: number | undefined;
|
||||||
|
let phoneNumber: number | undefined;
|
||||||
let username: number | undefined;
|
let username: number | undefined;
|
||||||
|
|
||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
|
@ -359,7 +358,10 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
group = rowCount;
|
group = rowCount;
|
||||||
rowCount += this.composeContacts.length;
|
rowCount += this.composeContacts.length;
|
||||||
}
|
}
|
||||||
if (this.getUsernameFromSearch()) {
|
if (this.phoneNumber) {
|
||||||
|
phoneNumber = rowCount;
|
||||||
|
}
|
||||||
|
if (this.username) {
|
||||||
username = rowCount;
|
username = rowCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,6 +369,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
top,
|
top,
|
||||||
contact,
|
contact,
|
||||||
group,
|
group,
|
||||||
|
phoneNumber,
|
||||||
username,
|
username,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import type { StatePropsType } from '../../components/conversation/conversation-
|
||||||
import { ChooseGroupMembersModal } from '../../components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal';
|
import { ChooseGroupMembersModal } from '../../components/conversation/conversation-details/AddGroupMembersModal/ChooseGroupMembersModal';
|
||||||
|
|
||||||
import { getIntl, getTheme, getRegionCode } from '../selectors/user';
|
import { getIntl, getTheme, getRegionCode } from '../selectors/user';
|
||||||
|
import { getUsernamesEnabled } from '../selectors/items';
|
||||||
import {
|
import {
|
||||||
getCandidateContactsForNewGroup,
|
getCandidateContactsForNewGroup,
|
||||||
getConversationByIdSelector,
|
getConversationByIdSelector,
|
||||||
|
@ -55,6 +56,7 @@ const mapStateToProps = (
|
||||||
theme: getTheme(state),
|
theme: getTheme(state),
|
||||||
selectedContacts,
|
selectedContacts,
|
||||||
lookupConversationWithoutUuid,
|
lookupConversationWithoutUuid,
|
||||||
|
isUsernamesEnabled: getUsernamesEnabled(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -156,6 +156,7 @@ const getModeSpecificProps = (
|
||||||
regionCode: getRegionCode(state),
|
regionCode: getRegionCode(state),
|
||||||
searchTerm: getComposerConversationSearchTerm(state),
|
searchTerm: getComposerConversationSearchTerm(state),
|
||||||
selectedContacts: getComposeSelectedContacts(state),
|
selectedContacts: getComposeSelectedContacts(state),
|
||||||
|
isUsernamesEnabled: getUsernamesEnabled(state),
|
||||||
uuidFetchState: getComposerUUIDFetchState(state),
|
uuidFetchState: getComposerUUIDFetchState(state),
|
||||||
};
|
};
|
||||||
case ComposerStep.SetGroupMetadata:
|
case ComposerStep.SetGroupMetadata:
|
||||||
|
|
|
@ -17,6 +17,7 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
|
||||||
candidateContacts: [],
|
candidateContacts: [],
|
||||||
isShowingRecommendedGroupSizeModal: false,
|
isShowingRecommendedGroupSizeModal: false,
|
||||||
isShowingMaximumGroupSizeModal: false,
|
isShowingMaximumGroupSizeModal: false,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
selectedContacts: [],
|
selectedContacts: [],
|
||||||
|
@ -62,6 +63,7 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
|
||||||
candidateContacts: [],
|
candidateContacts: [],
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
selectedContacts: [getDefaultConversation()],
|
selectedContacts: [getDefaultConversation()],
|
||||||
|
isUsernamesEnabled: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
@ -107,6 +109,7 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
|
||||||
candidateContacts: [],
|
candidateContacts: [],
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
selectedContacts: [getDefaultConversation()],
|
selectedContacts: [getDefaultConversation()],
|
||||||
|
isUsernamesEnabled: false,
|
||||||
}).getRow(0)
|
}).getRow(0)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -120,6 +123,7 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
|
||||||
...defaults,
|
...defaults,
|
||||||
candidateContacts,
|
candidateContacts,
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: false,
|
||||||
selectedContacts: [candidateContacts[1]],
|
selectedContacts: [candidateContacts[1]],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -164,5 +168,51 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
|
||||||
disabledReason: undefined,
|
disabledReason: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns a header, then the phone number, then a blank space if there are contacts', () => {
|
||||||
|
const helper = new LeftPaneChooseGroupMembersHelper({
|
||||||
|
...defaults,
|
||||||
|
candidateContacts: [],
|
||||||
|
searchTerm: '212 555',
|
||||||
|
selectedContacts: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
type: RowType.Header,
|
||||||
|
i18nKey: 'findByPhoneNumberHeader',
|
||||||
|
});
|
||||||
|
assert.deepEqual(helper.getRow(1), {
|
||||||
|
type: RowType.PhoneNumberCheckbox,
|
||||||
|
phoneNumber: {
|
||||||
|
isValid: false,
|
||||||
|
userInput: '212 555',
|
||||||
|
e164: '+1212555',
|
||||||
|
},
|
||||||
|
isChecked: false,
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(helper.getRow(2), { type: RowType.Blank });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a header, then the username, then a blank space if there are contacts', () => {
|
||||||
|
const helper = new LeftPaneChooseGroupMembersHelper({
|
||||||
|
...defaults,
|
||||||
|
candidateContacts: [],
|
||||||
|
searchTerm: 'signal',
|
||||||
|
selectedContacts: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
type: RowType.Header,
|
||||||
|
i18nKey: 'findByUsernameHeader',
|
||||||
|
});
|
||||||
|
assert.deepEqual(helper.getRow(1), {
|
||||||
|
type: RowType.UsernameCheckbox,
|
||||||
|
username: 'signal',
|
||||||
|
isChecked: false,
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
assert.deepEqual(helper.getRow(2), { type: RowType.Blank });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1626,7 +1626,7 @@ export function initialize({
|
||||||
return (await _ajax({
|
return (await _ajax({
|
||||||
call: 'profile',
|
call: 'profile',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
urlParameters: `username/${usernameToFetch}`,
|
urlParameters: `/username/${usernameToFetch}`,
|
||||||
responseType: 'json',
|
responseType: 'json',
|
||||||
redactUrl: _createRedactor(usernameToFetch),
|
redactUrl: _createRedactor(usernameToFetch),
|
||||||
})) as ProfileType;
|
})) as ProfileType;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue