Search for username in compose mode
This commit is contained in:
parent
6731cc6629
commit
cbae7f8ee9
36 changed files with 997 additions and 72 deletions
|
@ -54,6 +54,12 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
noteToSelf: boolean('noteToSelf', overrideProps.noteToSelf || false),
|
||||
onClick: action('onClick'),
|
||||
phoneNumber: text('phoneNumber', overrideProps.phoneNumber || ''),
|
||||
searchResult: boolean(
|
||||
'searchResult',
|
||||
typeof overrideProps.searchResult === 'boolean'
|
||||
? overrideProps.searchResult
|
||||
: false
|
||||
),
|
||||
sharedGroupNames: [],
|
||||
size: 80,
|
||||
title: overrideProps.title || '',
|
||||
|
@ -153,6 +159,14 @@ story.add('Group Icon', () => {
|
|||
return sizes.map(size => <Avatar key={size} {...props} size={size} />);
|
||||
});
|
||||
|
||||
story.add('Search Icon', () => {
|
||||
const props = createProps({
|
||||
searchResult: true,
|
||||
});
|
||||
|
||||
return sizes.map(size => <Avatar key={size} {...props} size={size} />);
|
||||
});
|
||||
|
||||
story.add('Colors', () => {
|
||||
const props = createProps();
|
||||
|
||||
|
|
|
@ -65,6 +65,7 @@ export type Props = {
|
|||
theme?: ThemeType;
|
||||
title: string;
|
||||
unblurredAvatarPath?: string;
|
||||
searchResult?: boolean;
|
||||
|
||||
onClick?: (event: MouseEvent<HTMLButtonElement>) => unknown;
|
||||
|
||||
|
@ -108,6 +109,7 @@ export const Avatar: FunctionComponent<Props> = ({
|
|||
theme,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
searchResult,
|
||||
blur = getDefaultBlur({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
|
@ -181,6 +183,15 @@ export const Avatar: FunctionComponent<Props> = ({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
} else if (searchResult) {
|
||||
contentsChildren = (
|
||||
<div
|
||||
className={classNames(
|
||||
'module-Avatar__icon',
|
||||
'module-Avatar__icon--search-result'
|
||||
)}
|
||||
/>
|
||||
);
|
||||
} else if (noteToSelf) {
|
||||
contentsChildren = (
|
||||
<div
|
||||
|
|
|
@ -84,6 +84,9 @@ const Wrapper = ({
|
|||
startNewConversationFromPhoneNumber={action(
|
||||
'startNewConversationFromPhoneNumber'
|
||||
)}
|
||||
startNewConversationFromUsername={action(
|
||||
'startNewConversationFromUsername'
|
||||
)}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
@ -492,6 +495,10 @@ story.add('Headers', () => (
|
|||
type: RowType.Header,
|
||||
i18nKey: 'messagesHeader',
|
||||
},
|
||||
{
|
||||
type: RowType.Header,
|
||||
i18nKey: 'findByUsernameHeader',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
@ -507,6 +514,27 @@ story.add('Start new conversation', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Find by username', () => (
|
||||
<Wrapper
|
||||
rows={[
|
||||
{
|
||||
type: RowType.Header,
|
||||
i18nKey: 'findByUsernameHeader',
|
||||
},
|
||||
{
|
||||
type: RowType.UsernameSearchResult,
|
||||
username: 'jowerty',
|
||||
isFetchingUsername: false,
|
||||
},
|
||||
{
|
||||
type: RowType.UsernameSearchResult,
|
||||
username: 'jowerty',
|
||||
isFetchingUsername: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Search results loading skeleton', () => (
|
||||
<Wrapper
|
||||
scrollable={false}
|
||||
|
@ -528,12 +556,16 @@ story.add('Kitchen sink', () => (
|
|||
},
|
||||
{
|
||||
type: RowType.Header,
|
||||
i18nKey: 'messagesHeader',
|
||||
i18nKey: 'contactsHeader',
|
||||
},
|
||||
{
|
||||
type: RowType.Contact,
|
||||
contact: defaultConversations[0],
|
||||
},
|
||||
{
|
||||
type: RowType.Header,
|
||||
i18nKey: 'messagesHeader',
|
||||
},
|
||||
{
|
||||
type: RowType.Conversation,
|
||||
conversation: defaultConversations[1],
|
||||
|
@ -542,6 +574,15 @@ story.add('Kitchen sink', () => (
|
|||
type: RowType.MessageSearchResult,
|
||||
messageId: '123',
|
||||
},
|
||||
{
|
||||
type: RowType.Header,
|
||||
i18nKey: 'findByUsernameHeader',
|
||||
},
|
||||
{
|
||||
type: RowType.UsernameSearchResult,
|
||||
username: 'jowerty',
|
||||
isFetchingUsername: false,
|
||||
},
|
||||
{
|
||||
type: RowType.ArchiveButton,
|
||||
archivedConversationsCount: 123,
|
||||
|
|
|
@ -26,6 +26,7 @@ import { CreateNewGroupButton } from './conversationList/CreateNewGroupButton';
|
|||
import { StartNewConversation as StartNewConversationComponent } from './conversationList/StartNewConversation';
|
||||
import { SearchResultsLoadingFakeHeader as SearchResultsLoadingFakeHeaderComponent } from './conversationList/SearchResultsLoadingFakeHeader';
|
||||
import { SearchResultsLoadingFakeRow as SearchResultsLoadingFakeRowComponent } from './conversationList/SearchResultsLoadingFakeRow';
|
||||
import { UsernameSearchResultListItem } from './conversationList/UsernameSearchResultListItem';
|
||||
|
||||
export enum RowType {
|
||||
ArchiveButton,
|
||||
|
@ -39,6 +40,7 @@ export enum RowType {
|
|||
SearchResultsLoadingFakeHeader,
|
||||
SearchResultsLoadingFakeRow,
|
||||
StartNewConversation,
|
||||
UsernameSearchResult,
|
||||
}
|
||||
|
||||
type ArchiveButtonRowType = {
|
||||
|
@ -93,6 +95,12 @@ type StartNewConversationRowType = {
|
|||
phoneNumber: string;
|
||||
};
|
||||
|
||||
type UsernameRowType = {
|
||||
type: RowType.UsernameSearchResult;
|
||||
username: string;
|
||||
isFetchingUsername: boolean;
|
||||
};
|
||||
|
||||
export type Row =
|
||||
| ArchiveButtonRowType
|
||||
| BlankRowType
|
||||
|
@ -104,7 +112,8 @@ export type Row =
|
|||
| HeaderRowType
|
||||
| SearchResultsLoadingFakeHeaderType
|
||||
| SearchResultsLoadingFakeRowType
|
||||
| StartNewConversationRowType;
|
||||
| StartNewConversationRowType
|
||||
| UsernameRowType;
|
||||
|
||||
export type PropsType = {
|
||||
badgesById?: Record<string, BadgeType>;
|
||||
|
@ -134,6 +143,7 @@ export type PropsType = {
|
|||
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||
showChooseGroupMembers: () => void;
|
||||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||
startNewConversationFromUsername: (username: string) => void;
|
||||
};
|
||||
|
||||
const NORMAL_ROW_HEIGHT = 76;
|
||||
|
@ -155,6 +165,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
shouldRecomputeRowHeights,
|
||||
showChooseGroupMembers,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startNewConversationFromUsername,
|
||||
theme,
|
||||
}) => {
|
||||
const listRef = useRef<null | List>(null);
|
||||
|
@ -327,6 +338,16 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
/>
|
||||
);
|
||||
break;
|
||||
case RowType.UsernameSearchResult:
|
||||
result = (
|
||||
<UsernameSearchResultListItem
|
||||
i18n={i18n}
|
||||
username={row.username}
|
||||
isFetchingUsername={row.isFetchingUsername}
|
||||
onClick={startNewConversationFromUsername}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(row);
|
||||
}
|
||||
|
@ -349,6 +370,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
|||
renderMessageSearchResult,
|
||||
showChooseGroupMembers,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startNewConversationFromUsername,
|
||||
theme,
|
||||
]
|
||||
);
|
||||
|
|
|
@ -420,6 +420,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
|||
startNewConversationFromPhoneNumber={
|
||||
shouldNeverBeCalled
|
||||
}
|
||||
startNewConversationFromUsername={shouldNeverBeCalled}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,9 +2,14 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import type { ContactModalStateType } from '../state/ducks/globalModals';
|
||||
import type {
|
||||
ContactModalStateType,
|
||||
UsernameNotFoundModalStateType,
|
||||
} from '../state/ducks/globalModals';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
|
||||
import { ButtonVariant } from './Button';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import { WhatsNewModal } from './WhatsNewModal';
|
||||
|
||||
type PropsType = {
|
||||
|
@ -18,6 +23,9 @@ type PropsType = {
|
|||
// SafetyNumberModal
|
||||
safetyNumberModalContactId?: string;
|
||||
renderSafetyNumber: () => JSX.Element;
|
||||
// UsernameNotFoundModal
|
||||
hideUsernameNotFoundModal: () => unknown;
|
||||
usernameNotFoundModalState?: UsernameNotFoundModalStateType;
|
||||
// WhatsNewModal
|
||||
isWhatsNewVisible: boolean;
|
||||
hideWhatsNewModal: () => unknown;
|
||||
|
@ -34,6 +42,9 @@ export const GlobalModalContainer = ({
|
|||
// SafetyNumberModal
|
||||
safetyNumberModalContactId,
|
||||
renderSafetyNumber,
|
||||
// UsernameNotFoundModal
|
||||
hideUsernameNotFoundModal,
|
||||
usernameNotFoundModalState,
|
||||
// WhatsNewModal
|
||||
hideWhatsNewModal,
|
||||
isWhatsNewVisible,
|
||||
|
@ -42,6 +53,23 @@ export const GlobalModalContainer = ({
|
|||
return renderSafetyNumber();
|
||||
}
|
||||
|
||||
if (usernameNotFoundModalState) {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
cancelText={i18n('ok')}
|
||||
cancelButtonVariant={ButtonVariant.Secondary}
|
||||
i18n={i18n}
|
||||
onClose={hideUsernameNotFoundModal}
|
||||
>
|
||||
{i18n('startConversation--username-not-found', {
|
||||
atUsername: i18n('at-username', {
|
||||
username: usernameNotFoundModalState.username,
|
||||
}),
|
||||
})}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
|
||||
if (contactModalState) {
|
||||
return renderContactModal();
|
||||
}
|
||||
|
|
|
@ -146,6 +146,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
startNewConversationFromPhoneNumber: action(
|
||||
'startNewConversationFromPhoneNumber'
|
||||
),
|
||||
startNewConversationFromUsername: action('startNewConversationFromUsername'),
|
||||
startSearch: action('startSearch'),
|
||||
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
|
@ -439,13 +440,15 @@ story.add('Archive: searching a conversation', () => (
|
|||
|
||||
// Compose stories
|
||||
|
||||
story.add('Compose: no contacts or groups', () => (
|
||||
story.add('Compose: no results', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: '',
|
||||
},
|
||||
|
@ -453,13 +456,15 @@ story.add('Compose: no contacts or groups', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: some contacts, no groups, no search term', () => (
|
||||
story.add('Compose: some contacts, no search term', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: defaultConversations,
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: '',
|
||||
},
|
||||
|
@ -467,13 +472,15 @@ story.add('Compose: some contacts, no groups, no search term', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: some contacts, no groups, with a search term', () => (
|
||||
story.add('Compose: some contacts, with a search term', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: defaultConversations,
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: 'ar',
|
||||
},
|
||||
|
@ -481,13 +488,15 @@ story.add('Compose: some contacts, no groups, with a search term', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: some groups, no contacts, no search term', () => (
|
||||
story.add('Compose: some groups, no search term', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: defaultGroups,
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: '',
|
||||
},
|
||||
|
@ -495,13 +504,15 @@ story.add('Compose: some groups, no contacts, no search term', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: some groups, no contacts, with search term', () => (
|
||||
story.add('Compose: some groups, with search term', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: defaultGroups,
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: 'ar',
|
||||
},
|
||||
|
@ -509,13 +520,63 @@ story.add('Compose: some groups, no contacts, with search term', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: some contacts, some groups, no search term', () => (
|
||||
story.add('Compose: search is valid username', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: 'someone',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: search is valid username, fetching username', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: true,
|
||||
regionCode: 'US',
|
||||
searchTerm: 'someone',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: search is valid username, but flag is not enabled', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: [],
|
||||
composeGroups: [],
|
||||
isUsernamesEnabled: false,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: 'someone',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: all kinds of results, no search term', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: defaultConversations,
|
||||
composeGroups: defaultGroups,
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: '',
|
||||
},
|
||||
|
@ -523,15 +584,17 @@ story.add('Compose: some contacts, some groups, no search term', () => (
|
|||
/>
|
||||
));
|
||||
|
||||
story.add('Compose: some contacts, some groups, with a search term', () => (
|
||||
story.add('Compose: all kinds of results, with a search term', () => (
|
||||
<LeftPane
|
||||
{...useProps({
|
||||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Compose,
|
||||
composeContacts: defaultConversations,
|
||||
composeGroups: defaultGroups,
|
||||
isUsernamesEnabled: true,
|
||||
isFetchingUsername: false,
|
||||
regionCode: 'US',
|
||||
searchTerm: 'ar',
|
||||
searchTerm: 'someone',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -103,6 +103,7 @@ export type PropsType = {
|
|||
closeRecommendedGroupSizeModal: () => void;
|
||||
createGroup: () => void;
|
||||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||
startNewConversationFromUsername: (username: string) => void;
|
||||
openConversationInternal: (_: {
|
||||
conversationId: string;
|
||||
messageId?: string;
|
||||
|
@ -185,6 +186,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
startComposing,
|
||||
startSearch,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startNewConversationFromUsername,
|
||||
startSettingGroupMetadata,
|
||||
theme,
|
||||
toggleComposeEditingAvatar,
|
||||
|
@ -607,6 +609,9 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
startNewConversationFromPhoneNumber={
|
||||
startNewConversationFromPhoneNumber
|
||||
}
|
||||
startNewConversationFromUsername={
|
||||
startNewConversationFromUsername
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import * as log from '../logging/log';
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
|
@ -627,8 +628,15 @@ export const ProfileEditor = ({
|
|||
value={newUsername}
|
||||
/>
|
||||
|
||||
<div className="ProfileEditor__error">{usernameError}</div>
|
||||
<div className="ProfileEditor__info">
|
||||
{usernameError && (
|
||||
<div className="ProfileEditor__error">{usernameError}</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'ProfileEditor__info',
|
||||
!usernameError ? 'ProfileEditor__info--no-error' : undefined
|
||||
)}
|
||||
>
|
||||
<Intl i18n={i18n} id="ProfileEditor--username--helper" />
|
||||
</div>
|
||||
|
||||
|
|
22
ts/components/ToastFailedToFetchUsername.tsx
Normal file
22
ts/components/ToastFailedToFetchUsername.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 ToastFailedToFetchUsername = ({
|
||||
i18n,
|
||||
onClose,
|
||||
}: PropsType): JSX.Element => {
|
||||
return (
|
||||
<Toast onClose={onClose} style={{ maxWidth: '280px' }}>
|
||||
{i18n('Toast--failed-to-fetch-username')}
|
||||
</Toast>
|
||||
);
|
||||
};
|
|
@ -229,6 +229,7 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
shouldRecomputeRowHeights={false}
|
||||
showChooseGroupMembers={shouldNeverBeCalled}
|
||||
startNewConversationFromPhoneNumber={shouldNeverBeCalled}
|
||||
startNewConversationFromUsername={shouldNeverBeCalled}
|
||||
theme={theme}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -14,6 +14,7 @@ import { isConversationUnread } from '../../util/isConversationUnread';
|
|||
import { cleanId } from '../_util';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
||||
const BASE_CLASS_NAME =
|
||||
'module-conversation-list__item--contact-or-conversation';
|
||||
|
@ -38,12 +39,14 @@ type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
isNoteToSelf?: boolean;
|
||||
isSelected: boolean;
|
||||
isUsernameSearchResult?: boolean;
|
||||
markedUnread?: boolean;
|
||||
messageId?: string;
|
||||
messageStatusIcon?: ReactNode;
|
||||
messageText?: ReactNode;
|
||||
messageTextIsAlwaysFullSize?: boolean;
|
||||
onClick?: () => void;
|
||||
shouldShowSpinner?: boolean;
|
||||
theme?: ThemeType;
|
||||
unreadCount?: number;
|
||||
} & Pick<
|
||||
|
@ -76,6 +79,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
id,
|
||||
isMe,
|
||||
isNoteToSelf,
|
||||
isUsernameSearchResult,
|
||||
isSelected,
|
||||
markedUnread,
|
||||
messageStatusIcon,
|
||||
|
@ -86,6 +90,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
shouldShowSpinner,
|
||||
theme,
|
||||
title,
|
||||
unblurredAvatarPath,
|
||||
|
@ -101,8 +106,12 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
|
||||
const isCheckbox = isBoolean(checked);
|
||||
|
||||
let checkboxNode: ReactNode;
|
||||
if (isCheckbox) {
|
||||
let actionNode: ReactNode;
|
||||
if (shouldShowSpinner) {
|
||||
actionNode = (
|
||||
<Spinner size="20px" svgSize="small" direction="on-progress-dialog" />
|
||||
);
|
||||
} else if (isCheckbox) {
|
||||
let ariaLabel: string;
|
||||
if (disabled) {
|
||||
ariaLabel = i18n('cannotSelectContact', [title]);
|
||||
|
@ -111,7 +120,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
} else {
|
||||
ariaLabel = i18n('selectContact', [title]);
|
||||
}
|
||||
checkboxNode = (
|
||||
actionNode = (
|
||||
<input
|
||||
aria-label={ariaLabel}
|
||||
checked={checked}
|
||||
|
@ -138,6 +147,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
color={color}
|
||||
conversationType={conversationType}
|
||||
noteToSelf={isAvatarNoteToSelf}
|
||||
searchResult={isUsernameSearchResult}
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
name={name}
|
||||
|
@ -187,7 +197,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{checkboxNode}
|
||||
{actionNode}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { FunctionComponent } from 'react';
|
||||
import React from 'react';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { BaseConversationListItem } from './BaseConversationListItem';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
|
||||
type PropsData = {
|
||||
username: string;
|
||||
isFetchingUsername: boolean;
|
||||
};
|
||||
|
||||
type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
onClick: (username: string) => void;
|
||||
};
|
||||
|
||||
export type Props = PropsData & PropsHousekeeping;
|
||||
|
||||
export const UsernameSearchResultListItem: FunctionComponent<Props> = ({
|
||||
i18n,
|
||||
isFetchingUsername,
|
||||
onClick,
|
||||
username,
|
||||
}) => {
|
||||
const usernameText = i18n('at-username', { username });
|
||||
const boundOnClick = isFetchingUsername
|
||||
? noop
|
||||
: () => {
|
||||
onClick(username);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseConversationListItem
|
||||
acceptedMessageRequest={false}
|
||||
conversationType="direct"
|
||||
headerName={usernameText}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
isSelected={false}
|
||||
isUsernameSearchResult
|
||||
shouldShowSpinner={isFetchingUsername}
|
||||
onClick={boundOnClick}
|
||||
sharedGroupNames={[]}
|
||||
title={usernameText}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -18,12 +18,16 @@ import {
|
|||
} from '../../util/libphonenumberInstance';
|
||||
import { assert } from '../../util/assert';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { getUsernameFromSearch } from '../../types/Username';
|
||||
|
||||
export type LeftPaneComposePropsType = {
|
||||
composeContacts: ReadonlyArray<ContactListItemPropsType>;
|
||||
composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
||||
|
||||
regionCode: string;
|
||||
searchTerm: string;
|
||||
isFetchingUsername: boolean;
|
||||
isUsernamesEnabled: boolean;
|
||||
};
|
||||
|
||||
enum TopButton {
|
||||
|
@ -37,6 +41,10 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
|
||||
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
||||
|
||||
private readonly isFetchingUsername: boolean;
|
||||
|
||||
private readonly isUsernamesEnabled: boolean;
|
||||
|
||||
private readonly searchTerm: string;
|
||||
|
||||
private readonly phoneNumber: undefined | PhoneNumber;
|
||||
|
@ -46,13 +54,17 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
composeGroups,
|
||||
regionCode,
|
||||
searchTerm,
|
||||
isUsernamesEnabled,
|
||||
isFetchingUsername,
|
||||
}: Readonly<LeftPaneComposePropsType>) {
|
||||
super();
|
||||
|
||||
this.composeContacts = composeContacts;
|
||||
this.composeGroups = composeGroups;
|
||||
this.searchTerm = searchTerm;
|
||||
this.phoneNumber = parsePhoneNumber(searchTerm, regionCode);
|
||||
this.composeGroups = composeGroups;
|
||||
this.composeContacts = composeContacts;
|
||||
this.isFetchingUsername = isFetchingUsername;
|
||||
this.isUsernamesEnabled = isUsernamesEnabled;
|
||||
}
|
||||
|
||||
getHeaderContents({
|
||||
|
@ -121,6 +133,9 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
if (this.hasGroupsHeader()) {
|
||||
result += 1;
|
||||
}
|
||||
if (this.getUsernameFromSearch()) {
|
||||
result += 2;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -187,10 +202,36 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
virtualRowIndex -= 1;
|
||||
|
||||
const group = this.composeGroups[virtualRowIndex];
|
||||
return {
|
||||
type: RowType.Conversation,
|
||||
conversation: group,
|
||||
};
|
||||
if (group) {
|
||||
return {
|
||||
type: RowType.Conversation,
|
||||
conversation: group,
|
||||
};
|
||||
}
|
||||
|
||||
virtualRowIndex -= this.composeGroups.length;
|
||||
}
|
||||
|
||||
const username = this.getUsernameFromSearch();
|
||||
if (username) {
|
||||
if (virtualRowIndex === 0) {
|
||||
return {
|
||||
type: RowType.Header,
|
||||
i18nKey: 'findByUsernameHeader',
|
||||
};
|
||||
}
|
||||
|
||||
virtualRowIndex -= 1;
|
||||
|
||||
if (virtualRowIndex === 0) {
|
||||
return {
|
||||
type: RowType.UsernameSearchResult,
|
||||
username,
|
||||
isFetchingUsername: this.isFetchingUsername,
|
||||
};
|
||||
|
||||
virtualRowIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -220,7 +261,8 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
return (
|
||||
currHeaderIndices.top !== prevHeaderIndices.top ||
|
||||
currHeaderIndices.contact !== prevHeaderIndices.contact ||
|
||||
currHeaderIndices.group !== prevHeaderIndices.group
|
||||
currHeaderIndices.group !== prevHeaderIndices.group ||
|
||||
currHeaderIndices.username !== prevHeaderIndices.username
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -246,31 +288,56 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
|||
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(): {
|
||||
top?: number;
|
||||
contact?: number;
|
||||
group?: number;
|
||||
username?: number;
|
||||
} {
|
||||
let top: number | undefined;
|
||||
let contact: number | undefined;
|
||||
let group: number | undefined;
|
||||
let username: number | undefined;
|
||||
|
||||
let rowCount = 0;
|
||||
|
||||
if (this.hasTopButton()) {
|
||||
top = 0;
|
||||
rowCount += 1;
|
||||
}
|
||||
if (this.composeContacts.length) {
|
||||
if (this.hasContactsHeader()) {
|
||||
contact = rowCount;
|
||||
rowCount += this.composeContacts.length;
|
||||
}
|
||||
if (this.composeGroups.length) {
|
||||
if (this.hasGroupsHeader()) {
|
||||
group = rowCount;
|
||||
rowCount += this.composeContacts.length;
|
||||
}
|
||||
if (this.getUsernameFromSearch()) {
|
||||
username = rowCount;
|
||||
}
|
||||
|
||||
return {
|
||||
top,
|
||||
contact,
|
||||
group,
|
||||
username,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue