Search for username in compose mode
This commit is contained in:
parent
6731cc6629
commit
cbae7f8ee9
|
@ -823,6 +823,20 @@
|
||||||
"message": "Messages",
|
"message": "Messages",
|
||||||
"description": "Shown to separate the types of search results"
|
"description": "Shown to separate the types of search results"
|
||||||
},
|
},
|
||||||
|
"findByUsernameHeader": {
|
||||||
|
"message": "Find by Username",
|
||||||
|
"description": "Shown to separate the types of search results"
|
||||||
|
},
|
||||||
|
"at-username": {
|
||||||
|
"message": "@$username$",
|
||||||
|
"description": "@ added to username to signify it as a username. Should it be on the right in your language?",
|
||||||
|
"placeholders": {
|
||||||
|
"username": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "sammy45"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"welcomeToSignal": {
|
"welcomeToSignal": {
|
||||||
"message": "Welcome to Signal"
|
"message": "Welcome to Signal"
|
||||||
},
|
},
|
||||||
|
@ -2238,6 +2252,20 @@
|
||||||
"message": "No conversations found",
|
"message": "No conversations found",
|
||||||
"description": "Label shown when there are no conversations to compose to"
|
"description": "Label shown when there are no conversations to compose to"
|
||||||
},
|
},
|
||||||
|
"Toast--failed-to-fetch-username": {
|
||||||
|
"message": "Failed to fetch username. Check your connection and try again.",
|
||||||
|
"description": "Shown if request to Signal servers to find username 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",
|
||||||
|
"placeholders": {
|
||||||
|
"atUsername": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "@alex"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"chooseGroupMembers__title": {
|
"chooseGroupMembers__title": {
|
||||||
"message": "Choose members",
|
"message": "Choose members",
|
||||||
"description": "The title for the 'choose group members' left pane screen"
|
"description": "The title for the 'choose group members' left pane screen"
|
||||||
|
@ -3285,7 +3313,7 @@
|
||||||
"message": "Unblock",
|
"message": "Unblock",
|
||||||
"description": "Shown as a button to let the user unblock a message request"
|
"description": "Shown as a button to let the user unblock a message request"
|
||||||
},
|
},
|
||||||
"MessageRequests--unblock-confirm-title": {
|
"MessageRequests--unblock-direct-confirm-title": {
|
||||||
"message": "Unblock $name$?",
|
"message": "Unblock $name$?",
|
||||||
"description": "Shown as a button to let the user unblock a message request",
|
"description": "Shown as a button to let the user unblock a message request",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
|
@ -6185,7 +6213,7 @@
|
||||||
"description": "Placeholder for the username field"
|
"description": "Placeholder for the username field"
|
||||||
},
|
},
|
||||||
"ProfileEditor--username--helper": {
|
"ProfileEditor--username--helper": {
|
||||||
"message": "Usernames on Signal are optional. If you choose to create a username and make it searchable, other Signal users will be able to find you by this username and contact you without knowing your phone number.",
|
"message": "Usernames on Signal are optional. If you choose to create a username other Signal users will be able to find you by this username and contact you without knowing your phone number.",
|
||||||
"description": "Shown on the edit username screen"
|
"description": "Shown on the edit username screen"
|
||||||
},
|
},
|
||||||
"ProfileEditor--username--check-characters": {
|
"ProfileEditor--username--check-characters": {
|
||||||
|
|
|
@ -38,6 +38,7 @@ $color-white-alpha-90: rgba($color-white, 0.9);
|
||||||
|
|
||||||
$color-black-alpha-05: rgba($color-black, 0.05);
|
$color-black-alpha-05: rgba($color-black, 0.05);
|
||||||
$color-black-alpha-06: rgba($color-black, 0.06);
|
$color-black-alpha-06: rgba($color-black, 0.06);
|
||||||
|
$color-black-alpha-08: rgba($color-black, 0.08);
|
||||||
$color-black-alpha-12: rgba($color-black, 0.12);
|
$color-black-alpha-12: rgba($color-black, 0.12);
|
||||||
$color-black-alpha-20: rgba($color-black, 0.2);
|
$color-black-alpha-20: rgba($color-black, 0.2);
|
||||||
$color-black-alpha-30: rgba($color-black, 0.3);
|
$color-black-alpha-30: rgba($color-black, 0.3);
|
||||||
|
|
|
@ -100,6 +100,11 @@
|
||||||
&--note-to-self {
|
&--note-to-self {
|
||||||
-webkit-mask-image: url('../images/icons/v2/note-24.svg');
|
-webkit-mask-image: url('../images/icons/v2/note-24.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--search-result {
|
||||||
|
-webkit-mask-image: url('../images/icons/v2/search-24.svg');
|
||||||
|
-webkit-mask-size: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__spinner-container {
|
&__spinner-container {
|
||||||
|
|
|
@ -98,5 +98,11 @@
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// To account for missing error section - 16px previous margin, 34px for
|
||||||
|
// 16px margin of error plus 18px line height.
|
||||||
|
&--no-error {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
@include font-body-2;
|
@include font-body-2;
|
||||||
|
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
background-color: $color-gray-02;
|
background-color: $color-black-alpha-08;
|
||||||
border: solid 1px $color-gray-02;
|
border: solid 1px $color-gray-02;
|
||||||
color: $color-gray-90;
|
color: $color-gray-90;
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__input {
|
&__input {
|
||||||
background: inherit;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
padding-left: 16px;
|
padding-left: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -952,6 +952,10 @@ export async function startApp(): Promise<void> {
|
||||||
conversations,
|
conversations,
|
||||||
'groupId'
|
'groupId'
|
||||||
),
|
),
|
||||||
|
conversationsByUsername: window.Signal.Util.makeLookup(
|
||||||
|
conversations,
|
||||||
|
'username'
|
||||||
|
),
|
||||||
messagesByConversation: {},
|
messagesByConversation: {},
|
||||||
messagesLookup: {},
|
messagesLookup: {},
|
||||||
outboundMessagesPendingConversationVerification: {},
|
outboundMessagesPendingConversationVerification: {},
|
||||||
|
|
|
@ -54,6 +54,12 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
noteToSelf: boolean('noteToSelf', overrideProps.noteToSelf || false),
|
noteToSelf: boolean('noteToSelf', overrideProps.noteToSelf || false),
|
||||||
onClick: action('onClick'),
|
onClick: action('onClick'),
|
||||||
phoneNumber: text('phoneNumber', overrideProps.phoneNumber || ''),
|
phoneNumber: text('phoneNumber', overrideProps.phoneNumber || ''),
|
||||||
|
searchResult: boolean(
|
||||||
|
'searchResult',
|
||||||
|
typeof overrideProps.searchResult === 'boolean'
|
||||||
|
? overrideProps.searchResult
|
||||||
|
: false
|
||||||
|
),
|
||||||
sharedGroupNames: [],
|
sharedGroupNames: [],
|
||||||
size: 80,
|
size: 80,
|
||||||
title: overrideProps.title || '',
|
title: overrideProps.title || '',
|
||||||
|
@ -153,6 +159,14 @@ story.add('Group Icon', () => {
|
||||||
return sizes.map(size => <Avatar key={size} {...props} size={size} />);
|
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', () => {
|
story.add('Colors', () => {
|
||||||
const props = createProps();
|
const props = createProps();
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ export type Props = {
|
||||||
theme?: ThemeType;
|
theme?: ThemeType;
|
||||||
title: string;
|
title: string;
|
||||||
unblurredAvatarPath?: string;
|
unblurredAvatarPath?: string;
|
||||||
|
searchResult?: boolean;
|
||||||
|
|
||||||
onClick?: (event: MouseEvent<HTMLButtonElement>) => unknown;
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => unknown;
|
||||||
|
|
||||||
|
@ -108,6 +109,7 @@ export const Avatar: FunctionComponent<Props> = ({
|
||||||
theme,
|
theme,
|
||||||
title,
|
title,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
|
searchResult,
|
||||||
blur = getDefaultBlur({
|
blur = getDefaultBlur({
|
||||||
acceptedMessageRequest,
|
acceptedMessageRequest,
|
||||||
avatarPath,
|
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) {
|
} else if (noteToSelf) {
|
||||||
contentsChildren = (
|
contentsChildren = (
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -84,6 +84,9 @@ const Wrapper = ({
|
||||||
startNewConversationFromPhoneNumber={action(
|
startNewConversationFromPhoneNumber={action(
|
||||||
'startNewConversationFromPhoneNumber'
|
'startNewConversationFromPhoneNumber'
|
||||||
)}
|
)}
|
||||||
|
startNewConversationFromUsername={action(
|
||||||
|
'startNewConversationFromUsername'
|
||||||
|
)}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -492,6 +495,10 @@ story.add('Headers', () => (
|
||||||
type: RowType.Header,
|
type: RowType.Header,
|
||||||
i18nKey: 'messagesHeader',
|
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', () => (
|
story.add('Search results loading skeleton', () => (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
scrollable={false}
|
scrollable={false}
|
||||||
|
@ -528,12 +556,16 @@ story.add('Kitchen sink', () => (
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: RowType.Header,
|
type: RowType.Header,
|
||||||
i18nKey: 'messagesHeader',
|
i18nKey: 'contactsHeader',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: RowType.Contact,
|
type: RowType.Contact,
|
||||||
contact: defaultConversations[0],
|
contact: defaultConversations[0],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: RowType.Header,
|
||||||
|
i18nKey: 'messagesHeader',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: RowType.Conversation,
|
type: RowType.Conversation,
|
||||||
conversation: defaultConversations[1],
|
conversation: defaultConversations[1],
|
||||||
|
@ -542,6 +574,15 @@ story.add('Kitchen sink', () => (
|
||||||
type: RowType.MessageSearchResult,
|
type: RowType.MessageSearchResult,
|
||||||
messageId: '123',
|
messageId: '123',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: RowType.Header,
|
||||||
|
i18nKey: 'findByUsernameHeader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: RowType.UsernameSearchResult,
|
||||||
|
username: 'jowerty',
|
||||||
|
isFetchingUsername: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: RowType.ArchiveButton,
|
type: RowType.ArchiveButton,
|
||||||
archivedConversationsCount: 123,
|
archivedConversationsCount: 123,
|
||||||
|
|
|
@ -26,6 +26,7 @@ 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';
|
||||||
import { SearchResultsLoadingFakeRow as SearchResultsLoadingFakeRowComponent } from './conversationList/SearchResultsLoadingFakeRow';
|
import { SearchResultsLoadingFakeRow as SearchResultsLoadingFakeRowComponent } from './conversationList/SearchResultsLoadingFakeRow';
|
||||||
|
import { UsernameSearchResultListItem } from './conversationList/UsernameSearchResultListItem';
|
||||||
|
|
||||||
export enum RowType {
|
export enum RowType {
|
||||||
ArchiveButton,
|
ArchiveButton,
|
||||||
|
@ -39,6 +40,7 @@ export enum RowType {
|
||||||
SearchResultsLoadingFakeHeader,
|
SearchResultsLoadingFakeHeader,
|
||||||
SearchResultsLoadingFakeRow,
|
SearchResultsLoadingFakeRow,
|
||||||
StartNewConversation,
|
StartNewConversation,
|
||||||
|
UsernameSearchResult,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchiveButtonRowType = {
|
type ArchiveButtonRowType = {
|
||||||
|
@ -93,6 +95,12 @@ type StartNewConversationRowType = {
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UsernameRowType = {
|
||||||
|
type: RowType.UsernameSearchResult;
|
||||||
|
username: string;
|
||||||
|
isFetchingUsername: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type Row =
|
export type Row =
|
||||||
| ArchiveButtonRowType
|
| ArchiveButtonRowType
|
||||||
| BlankRowType
|
| BlankRowType
|
||||||
|
@ -104,7 +112,8 @@ export type Row =
|
||||||
| HeaderRowType
|
| HeaderRowType
|
||||||
| SearchResultsLoadingFakeHeaderType
|
| SearchResultsLoadingFakeHeaderType
|
||||||
| SearchResultsLoadingFakeRowType
|
| SearchResultsLoadingFakeRowType
|
||||||
| StartNewConversationRowType;
|
| StartNewConversationRowType
|
||||||
|
| UsernameRowType;
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
badgesById?: Record<string, BadgeType>;
|
badgesById?: Record<string, BadgeType>;
|
||||||
|
@ -134,6 +143,7 @@ export type PropsType = {
|
||||||
renderMessageSearchResult: (id: string) => JSX.Element;
|
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||||
showChooseGroupMembers: () => void;
|
showChooseGroupMembers: () => void;
|
||||||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||||
|
startNewConversationFromUsername: (username: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NORMAL_ROW_HEIGHT = 76;
|
const NORMAL_ROW_HEIGHT = 76;
|
||||||
|
@ -155,6 +165,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
||||||
shouldRecomputeRowHeights,
|
shouldRecomputeRowHeights,
|
||||||
showChooseGroupMembers,
|
showChooseGroupMembers,
|
||||||
startNewConversationFromPhoneNumber,
|
startNewConversationFromPhoneNumber,
|
||||||
|
startNewConversationFromUsername,
|
||||||
theme,
|
theme,
|
||||||
}) => {
|
}) => {
|
||||||
const listRef = useRef<null | List>(null);
|
const listRef = useRef<null | List>(null);
|
||||||
|
@ -327,6 +338,16 @@ export const ConversationList: React.FC<PropsType> = ({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case RowType.UsernameSearchResult:
|
||||||
|
result = (
|
||||||
|
<UsernameSearchResultListItem
|
||||||
|
i18n={i18n}
|
||||||
|
username={row.username}
|
||||||
|
isFetchingUsername={row.isFetchingUsername}
|
||||||
|
onClick={startNewConversationFromUsername}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw missingCaseError(row);
|
throw missingCaseError(row);
|
||||||
}
|
}
|
||||||
|
@ -349,6 +370,7 @@ export const ConversationList: React.FC<PropsType> = ({
|
||||||
renderMessageSearchResult,
|
renderMessageSearchResult,
|
||||||
showChooseGroupMembers,
|
showChooseGroupMembers,
|
||||||
startNewConversationFromPhoneNumber,
|
startNewConversationFromPhoneNumber,
|
||||||
|
startNewConversationFromUsername,
|
||||||
theme,
|
theme,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -420,6 +420,7 @@ export const ForwardMessageModal: FunctionComponent<PropsType> = ({
|
||||||
startNewConversationFromPhoneNumber={
|
startNewConversationFromPhoneNumber={
|
||||||
shouldNeverBeCalled
|
shouldNeverBeCalled
|
||||||
}
|
}
|
||||||
|
startNewConversationFromUsername={shouldNeverBeCalled}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,9 +2,14 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
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 type { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
|
import { ButtonVariant } from './Button';
|
||||||
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import { WhatsNewModal } from './WhatsNewModal';
|
import { WhatsNewModal } from './WhatsNewModal';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
|
@ -18,6 +23,9 @@ type PropsType = {
|
||||||
// SafetyNumberModal
|
// SafetyNumberModal
|
||||||
safetyNumberModalContactId?: string;
|
safetyNumberModalContactId?: string;
|
||||||
renderSafetyNumber: () => JSX.Element;
|
renderSafetyNumber: () => JSX.Element;
|
||||||
|
// UsernameNotFoundModal
|
||||||
|
hideUsernameNotFoundModal: () => unknown;
|
||||||
|
usernameNotFoundModalState?: UsernameNotFoundModalStateType;
|
||||||
// WhatsNewModal
|
// WhatsNewModal
|
||||||
isWhatsNewVisible: boolean;
|
isWhatsNewVisible: boolean;
|
||||||
hideWhatsNewModal: () => unknown;
|
hideWhatsNewModal: () => unknown;
|
||||||
|
@ -34,6 +42,9 @@ export const GlobalModalContainer = ({
|
||||||
// SafetyNumberModal
|
// SafetyNumberModal
|
||||||
safetyNumberModalContactId,
|
safetyNumberModalContactId,
|
||||||
renderSafetyNumber,
|
renderSafetyNumber,
|
||||||
|
// UsernameNotFoundModal
|
||||||
|
hideUsernameNotFoundModal,
|
||||||
|
usernameNotFoundModalState,
|
||||||
// WhatsNewModal
|
// WhatsNewModal
|
||||||
hideWhatsNewModal,
|
hideWhatsNewModal,
|
||||||
isWhatsNewVisible,
|
isWhatsNewVisible,
|
||||||
|
@ -42,6 +53,23 @@ export const GlobalModalContainer = ({
|
||||||
return renderSafetyNumber();
|
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) {
|
if (contactModalState) {
|
||||||
return renderContactModal();
|
return renderContactModal();
|
||||||
}
|
}
|
||||||
|
|
|
@ -146,6 +146,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
startNewConversationFromPhoneNumber: action(
|
startNewConversationFromPhoneNumber: action(
|
||||||
'startNewConversationFromPhoneNumber'
|
'startNewConversationFromPhoneNumber'
|
||||||
),
|
),
|
||||||
|
startNewConversationFromUsername: action('startNewConversationFromUsername'),
|
||||||
startSearch: action('startSearch'),
|
startSearch: action('startSearch'),
|
||||||
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
||||||
theme: React.useContext(StorybookThemeContext),
|
theme: React.useContext(StorybookThemeContext),
|
||||||
|
@ -439,13 +440,15 @@ story.add('Archive: searching a conversation', () => (
|
||||||
|
|
||||||
// Compose stories
|
// Compose stories
|
||||||
|
|
||||||
story.add('Compose: no contacts or groups', () => (
|
story.add('Compose: no results', () => (
|
||||||
<LeftPane
|
<LeftPane
|
||||||
{...useProps({
|
{...useProps({
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Compose,
|
mode: LeftPaneMode.Compose,
|
||||||
composeContacts: [],
|
composeContacts: [],
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
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
|
<LeftPane
|
||||||
{...useProps({
|
{...useProps({
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Compose,
|
mode: LeftPaneMode.Compose,
|
||||||
composeContacts: defaultConversations,
|
composeContacts: defaultConversations,
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
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
|
<LeftPane
|
||||||
{...useProps({
|
{...useProps({
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Compose,
|
mode: LeftPaneMode.Compose,
|
||||||
composeContacts: defaultConversations,
|
composeContacts: defaultConversations,
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'ar',
|
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
|
<LeftPane
|
||||||
{...useProps({
|
{...useProps({
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Compose,
|
mode: LeftPaneMode.Compose,
|
||||||
composeContacts: [],
|
composeContacts: [],
|
||||||
composeGroups: defaultGroups,
|
composeGroups: defaultGroups,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
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
|
<LeftPane
|
||||||
{...useProps({
|
{...useProps({
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Compose,
|
mode: LeftPaneMode.Compose,
|
||||||
composeContacts: [],
|
composeContacts: [],
|
||||||
composeGroups: defaultGroups,
|
composeGroups: defaultGroups,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'ar',
|
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
|
<LeftPane
|
||||||
{...useProps({
|
{...useProps({
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Compose,
|
mode: LeftPaneMode.Compose,
|
||||||
composeContacts: defaultConversations,
|
composeContacts: defaultConversations,
|
||||||
composeGroups: defaultGroups,
|
composeGroups: defaultGroups,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
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
|
<LeftPane
|
||||||
{...useProps({
|
{...useProps({
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Compose,
|
mode: LeftPaneMode.Compose,
|
||||||
composeContacts: defaultConversations,
|
composeContacts: defaultConversations,
|
||||||
composeGroups: defaultGroups,
|
composeGroups: defaultGroups,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'ar',
|
searchTerm: 'someone',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -103,6 +103,7 @@ export type PropsType = {
|
||||||
closeRecommendedGroupSizeModal: () => void;
|
closeRecommendedGroupSizeModal: () => void;
|
||||||
createGroup: () => void;
|
createGroup: () => void;
|
||||||
startNewConversationFromPhoneNumber: (e164: string) => void;
|
startNewConversationFromPhoneNumber: (e164: string) => void;
|
||||||
|
startNewConversationFromUsername: (username: string) => void;
|
||||||
openConversationInternal: (_: {
|
openConversationInternal: (_: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
|
@ -185,6 +186,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
startComposing,
|
startComposing,
|
||||||
startSearch,
|
startSearch,
|
||||||
startNewConversationFromPhoneNumber,
|
startNewConversationFromPhoneNumber,
|
||||||
|
startNewConversationFromUsername,
|
||||||
startSettingGroupMetadata,
|
startSettingGroupMetadata,
|
||||||
theme,
|
theme,
|
||||||
toggleComposeEditingAvatar,
|
toggleComposeEditingAvatar,
|
||||||
|
@ -607,6 +609,9 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
startNewConversationFromPhoneNumber={
|
startNewConversationFromPhoneNumber={
|
||||||
startNewConversationFromPhoneNumber
|
startNewConversationFromPhoneNumber
|
||||||
}
|
}
|
||||||
|
startNewConversationFromUsername={
|
||||||
|
startNewConversationFromUsername
|
||||||
|
}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type { AvatarColorType } from '../types/Colors';
|
import type { AvatarColorType } from '../types/Colors';
|
||||||
|
@ -627,8 +628,15 @@ export const ProfileEditor = ({
|
||||||
value={newUsername}
|
value={newUsername}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="ProfileEditor__error">{usernameError}</div>
|
{usernameError && (
|
||||||
<div className="ProfileEditor__info">
|
<div className="ProfileEditor__error">{usernameError}</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'ProfileEditor__info',
|
||||||
|
!usernameError ? 'ProfileEditor__info--no-error' : undefined
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Intl i18n={i18n} id="ProfileEditor--username--helper" />
|
<Intl i18n={i18n} id="ProfileEditor--username--helper" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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}
|
shouldRecomputeRowHeights={false}
|
||||||
showChooseGroupMembers={shouldNeverBeCalled}
|
showChooseGroupMembers={shouldNeverBeCalled}
|
||||||
startNewConversationFromPhoneNumber={shouldNeverBeCalled}
|
startNewConversationFromPhoneNumber={shouldNeverBeCalled}
|
||||||
|
startNewConversationFromUsername={shouldNeverBeCalled}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { isConversationUnread } from '../../util/isConversationUnread';
|
||||||
import { cleanId } from '../_util';
|
import { cleanId } from '../_util';
|
||||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
import type { ConversationType } from '../../state/ducks/conversations';
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
import { Spinner } from '../Spinner';
|
||||||
|
|
||||||
const BASE_CLASS_NAME =
|
const BASE_CLASS_NAME =
|
||||||
'module-conversation-list__item--contact-or-conversation';
|
'module-conversation-list__item--contact-or-conversation';
|
||||||
|
@ -38,12 +39,14 @@ type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isNoteToSelf?: boolean;
|
isNoteToSelf?: boolean;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
isUsernameSearchResult?: boolean;
|
||||||
markedUnread?: boolean;
|
markedUnread?: boolean;
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
messageStatusIcon?: ReactNode;
|
messageStatusIcon?: ReactNode;
|
||||||
messageText?: ReactNode;
|
messageText?: ReactNode;
|
||||||
messageTextIsAlwaysFullSize?: boolean;
|
messageTextIsAlwaysFullSize?: boolean;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
shouldShowSpinner?: boolean;
|
||||||
theme?: ThemeType;
|
theme?: ThemeType;
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
|
@ -76,6 +79,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||||
id,
|
id,
|
||||||
isMe,
|
isMe,
|
||||||
isNoteToSelf,
|
isNoteToSelf,
|
||||||
|
isUsernameSearchResult,
|
||||||
isSelected,
|
isSelected,
|
||||||
markedUnread,
|
markedUnread,
|
||||||
messageStatusIcon,
|
messageStatusIcon,
|
||||||
|
@ -86,6 +90,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
profileName,
|
profileName,
|
||||||
sharedGroupNames,
|
sharedGroupNames,
|
||||||
|
shouldShowSpinner,
|
||||||
theme,
|
theme,
|
||||||
title,
|
title,
|
||||||
unblurredAvatarPath,
|
unblurredAvatarPath,
|
||||||
|
@ -101,8 +106,12 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||||
|
|
||||||
const isCheckbox = isBoolean(checked);
|
const isCheckbox = isBoolean(checked);
|
||||||
|
|
||||||
let checkboxNode: ReactNode;
|
let actionNode: ReactNode;
|
||||||
if (isCheckbox) {
|
if (shouldShowSpinner) {
|
||||||
|
actionNode = (
|
||||||
|
<Spinner size="20px" svgSize="small" direction="on-progress-dialog" />
|
||||||
|
);
|
||||||
|
} else if (isCheckbox) {
|
||||||
let ariaLabel: string;
|
let ariaLabel: string;
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
ariaLabel = i18n('cannotSelectContact', [title]);
|
ariaLabel = i18n('cannotSelectContact', [title]);
|
||||||
|
@ -111,7 +120,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||||
} else {
|
} else {
|
||||||
ariaLabel = i18n('selectContact', [title]);
|
ariaLabel = i18n('selectContact', [title]);
|
||||||
}
|
}
|
||||||
checkboxNode = (
|
actionNode = (
|
||||||
<input
|
<input
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
@ -138,6 +147,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||||
color={color}
|
color={color}
|
||||||
conversationType={conversationType}
|
conversationType={conversationType}
|
||||||
noteToSelf={isAvatarNoteToSelf}
|
noteToSelf={isAvatarNoteToSelf}
|
||||||
|
searchResult={isUsernameSearchResult}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
isMe={isMe}
|
isMe={isMe}
|
||||||
name={name}
|
name={name}
|
||||||
|
@ -187,7 +197,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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';
|
} from '../../util/libphonenumberInstance';
|
||||||
import { assert } from '../../util/assert';
|
import { assert } from '../../util/assert';
|
||||||
import { missingCaseError } from '../../util/missingCaseError';
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
import { getUsernameFromSearch } from '../../types/Username';
|
||||||
|
|
||||||
export type LeftPaneComposePropsType = {
|
export type LeftPaneComposePropsType = {
|
||||||
composeContacts: ReadonlyArray<ContactListItemPropsType>;
|
composeContacts: ReadonlyArray<ContactListItemPropsType>;
|
||||||
composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
||||||
|
|
||||||
regionCode: string;
|
regionCode: string;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
isFetchingUsername: boolean;
|
||||||
|
isUsernamesEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum TopButton {
|
enum TopButton {
|
||||||
|
@ -37,6 +41,10 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
|
|
||||||
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
private readonly composeGroups: ReadonlyArray<ConversationListItemPropsType>;
|
||||||
|
|
||||||
|
private readonly isFetchingUsername: boolean;
|
||||||
|
|
||||||
|
private readonly isUsernamesEnabled: boolean;
|
||||||
|
|
||||||
private readonly searchTerm: string;
|
private readonly searchTerm: string;
|
||||||
|
|
||||||
private readonly phoneNumber: undefined | PhoneNumber;
|
private readonly phoneNumber: undefined | PhoneNumber;
|
||||||
|
@ -46,13 +54,17 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
composeGroups,
|
composeGroups,
|
||||||
regionCode,
|
regionCode,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
isUsernamesEnabled,
|
||||||
|
isFetchingUsername,
|
||||||
}: Readonly<LeftPaneComposePropsType>) {
|
}: Readonly<LeftPaneComposePropsType>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
this.composeContacts = composeContacts;
|
||||||
|
this.composeGroups = composeGroups;
|
||||||
this.searchTerm = searchTerm;
|
this.searchTerm = searchTerm;
|
||||||
this.phoneNumber = parsePhoneNumber(searchTerm, regionCode);
|
this.phoneNumber = parsePhoneNumber(searchTerm, regionCode);
|
||||||
this.composeGroups = composeGroups;
|
this.isFetchingUsername = isFetchingUsername;
|
||||||
this.composeContacts = composeContacts;
|
this.isUsernamesEnabled = isUsernamesEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaderContents({
|
getHeaderContents({
|
||||||
|
@ -121,6 +133,9 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
if (this.hasGroupsHeader()) {
|
if (this.hasGroupsHeader()) {
|
||||||
result += 1;
|
result += 1;
|
||||||
}
|
}
|
||||||
|
if (this.getUsernameFromSearch()) {
|
||||||
|
result += 2;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -187,10 +202,36 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
virtualRowIndex -= 1;
|
virtualRowIndex -= 1;
|
||||||
|
|
||||||
const group = this.composeGroups[virtualRowIndex];
|
const group = this.composeGroups[virtualRowIndex];
|
||||||
return {
|
if (group) {
|
||||||
type: RowType.Conversation,
|
return {
|
||||||
conversation: group,
|
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;
|
return undefined;
|
||||||
|
@ -220,7 +261,8 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
|
||||||
return (
|
return (
|
||||||
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,31 +288,56 @@ 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;
|
||||||
|
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 username: number | undefined;
|
||||||
|
|
||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
|
|
||||||
if (this.hasTopButton()) {
|
if (this.hasTopButton()) {
|
||||||
top = 0;
|
top = 0;
|
||||||
rowCount += 1;
|
rowCount += 1;
|
||||||
}
|
}
|
||||||
if (this.composeContacts.length) {
|
if (this.hasContactsHeader()) {
|
||||||
contact = rowCount;
|
contact = rowCount;
|
||||||
rowCount += this.composeContacts.length;
|
rowCount += this.composeContacts.length;
|
||||||
}
|
}
|
||||||
if (this.composeGroups.length) {
|
if (this.hasGroupsHeader()) {
|
||||||
group = rowCount;
|
group = rowCount;
|
||||||
|
rowCount += this.composeContacts.length;
|
||||||
|
}
|
||||||
|
if (this.getUsernameFromSearch()) {
|
||||||
|
username = rowCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
top,
|
top,
|
||||||
contact,
|
contact,
|
||||||
group,
|
group,
|
||||||
|
username,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4478,10 +4478,13 @@ export class ConversationModel extends window.Backbone
|
||||||
|
|
||||||
getTitle(): string {
|
getTitle(): string {
|
||||||
if (isDirectConversation(this.attributes)) {
|
if (isDirectConversation(this.attributes)) {
|
||||||
|
const username = this.get('username');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.get('name') ||
|
this.get('name') ||
|
||||||
this.getProfileName() ||
|
this.getProfileName() ||
|
||||||
this.getNumber() ||
|
this.getNumber() ||
|
||||||
|
(username && window.i18n('at-username', { username })) ||
|
||||||
window.i18n('unknownContact')
|
window.i18n('unknownContact')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,6 @@
|
||||||
// Matching Whisper.events.trigger API
|
// Matching Whisper.events.trigger API
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
|
||||||
export function trigger(name: string, param1?: any, param2?: any): void {
|
export function trigger(name: string, ...rest: Array<any>): void {
|
||||||
window.Whisper.events.trigger(name, param1, param2);
|
window.Whisper.events.trigger(name, ...rest);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,14 @@ import { getOwn } from '../../util/getOwn';
|
||||||
import { assert, strictAssert } from '../../util/assert';
|
import { assert, strictAssert } from '../../util/assert';
|
||||||
import * as universalExpireTimer from '../../util/universalExpireTimer';
|
import * as universalExpireTimer from '../../util/universalExpireTimer';
|
||||||
import { trigger } from '../../shims/events';
|
import { trigger } from '../../shims/events';
|
||||||
import type { ToggleProfileEditorErrorActionType } from './globalModals';
|
import type {
|
||||||
import { TOGGLE_PROFILE_EDITOR_ERROR } from './globalModals';
|
ShowUsernameNotFoundModalActionType,
|
||||||
|
ToggleProfileEditorErrorActionType,
|
||||||
|
} from './globalModals';
|
||||||
|
import {
|
||||||
|
TOGGLE_PROFILE_EDITOR_ERROR,
|
||||||
|
actions as globalModalActions,
|
||||||
|
} from './globalModals';
|
||||||
import { isRecord } from '../../util/isRecord';
|
import { isRecord } from '../../util/isRecord';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
@ -41,6 +47,7 @@ import type { BodyRangeType } from '../../types/Util';
|
||||||
import { CallMode } from '../../types/Calling';
|
import { CallMode } from '../../types/Calling';
|
||||||
import type { MediaItemType } from '../../types/MediaItem';
|
import type { MediaItemType } from '../../types/MediaItem';
|
||||||
import type { UUIDStringType } from '../../types/UUID';
|
import type { UUIDStringType } from '../../types/UUID';
|
||||||
|
import { UUID } from '../../types/UUID';
|
||||||
import {
|
import {
|
||||||
getGroupSizeRecommendedLimit,
|
getGroupSizeRecommendedLimit,
|
||||||
getGroupSizeHardLimit,
|
getGroupSizeHardLimit,
|
||||||
|
@ -53,6 +60,7 @@ import { ContactSpoofingType } from '../../util/contactSpoofing';
|
||||||
import { writeProfile } from '../../services/writeProfile';
|
import { writeProfile } from '../../services/writeProfile';
|
||||||
import { writeUsername } from '../../services/writeUsername';
|
import { writeUsername } from '../../services/writeUsername';
|
||||||
import {
|
import {
|
||||||
|
getConversationsByUsername,
|
||||||
getMe,
|
getMe,
|
||||||
getMessageIdsPendingBecauseOfVerification,
|
getMessageIdsPendingBecauseOfVerification,
|
||||||
getUsernameSaveState,
|
getUsernameSaveState,
|
||||||
|
@ -69,6 +77,8 @@ import {
|
||||||
} from './conversationsEnums';
|
} from './conversationsEnums';
|
||||||
import { showToast } from '../../util/showToast';
|
import { showToast } from '../../util/showToast';
|
||||||
import { ToastFailedToDeleteUsername } from '../../components/ToastFailedToDeleteUsername';
|
import { ToastFailedToDeleteUsername } from '../../components/ToastFailedToDeleteUsername';
|
||||||
|
import { ToastFailedToFetchUsername } from '../../components/ToastFailedToFetchUsername';
|
||||||
|
import { isValidUsername } from '../../types/Username';
|
||||||
|
|
||||||
import type { NoopActionType } from './noop';
|
import type { NoopActionType } from './noop';
|
||||||
|
|
||||||
|
@ -278,10 +288,16 @@ type ComposerGroupCreationState = {
|
||||||
userAvatarData: Array<AvatarDataType>;
|
userAvatarData: Array<AvatarDataType>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FoundUsernameType = {
|
||||||
|
uuid: UUIDStringType;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
type ComposerStateType =
|
type ComposerStateType =
|
||||||
| {
|
| {
|
||||||
step: ComposerStep.StartDirectConversation;
|
step: ComposerStep.StartDirectConversation;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
isFetchingUsername: boolean;
|
||||||
}
|
}
|
||||||
| ({
|
| ({
|
||||||
step: ComposerStep.ChooseGroupMembers;
|
step: ComposerStep.ChooseGroupMembers;
|
||||||
|
@ -314,6 +330,7 @@ export type ConversationsStateType = {
|
||||||
conversationsByE164: ConversationLookupType;
|
conversationsByE164: ConversationLookupType;
|
||||||
conversationsByUuid: ConversationLookupType;
|
conversationsByUuid: ConversationLookupType;
|
||||||
conversationsByGroupId: ConversationLookupType;
|
conversationsByGroupId: ConversationLookupType;
|
||||||
|
conversationsByUsername: ConversationLookupType;
|
||||||
selectedConversationId?: string;
|
selectedConversationId?: string;
|
||||||
selectedMessage?: string;
|
selectedMessage?: string;
|
||||||
selectedMessageCounter: number;
|
selectedMessageCounter: number;
|
||||||
|
@ -676,6 +693,12 @@ type SetComposeSearchTermActionType = {
|
||||||
type: 'SET_COMPOSE_SEARCH_TERM';
|
type: 'SET_COMPOSE_SEARCH_TERM';
|
||||||
payload: { searchTerm: string };
|
payload: { searchTerm: string };
|
||||||
};
|
};
|
||||||
|
type SetIsFetchingUsernameActionType = {
|
||||||
|
type: 'SET_IS_FETCHING_USERNAME';
|
||||||
|
payload: {
|
||||||
|
isFetchingUsername: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
type SetRecentMediaItemsActionType = {
|
type SetRecentMediaItemsActionType = {
|
||||||
type: 'SET_RECENT_MEDIA_ITEMS';
|
type: 'SET_RECENT_MEDIA_ITEMS';
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -768,6 +791,7 @@ export type ConversationActionType =
|
||||||
| SetComposeGroupNameActionType
|
| SetComposeGroupNameActionType
|
||||||
| SetComposeSearchTermActionType
|
| SetComposeSearchTermActionType
|
||||||
| SetConversationHeaderTitleActionType
|
| SetConversationHeaderTitleActionType
|
||||||
|
| SetIsFetchingUsernameActionType
|
||||||
| SetIsNearBottomActionType
|
| SetIsNearBottomActionType
|
||||||
| SetLoadCountdownStartActionType
|
| SetLoadCountdownStartActionType
|
||||||
| SetMessagesLoadingActionType
|
| SetMessagesLoadingActionType
|
||||||
|
@ -850,6 +874,7 @@ export const actions = {
|
||||||
showInbox,
|
showInbox,
|
||||||
startComposing,
|
startComposing,
|
||||||
startNewConversationFromPhoneNumber,
|
startNewConversationFromPhoneNumber,
|
||||||
|
startNewConversationFromUsername,
|
||||||
startSettingGroupMetadata,
|
startSettingGroupMetadata,
|
||||||
toggleAdmin,
|
toggleAdmin,
|
||||||
toggleConversationInChooseMembers,
|
toggleConversationInChooseMembers,
|
||||||
|
@ -1793,6 +1818,111 @@ function startNewConversationFromPhoneNumber(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function checkForUsername(
|
||||||
|
username: string
|
||||||
|
): Promise<FoundUsernameType | undefined> {
|
||||||
|
if (!isValidUsername(username)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const profile = await window.textsecure.messaging.getProfileForUsername(
|
||||||
|
username
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!profile.username || profile.username !== username) {
|
||||||
|
log.error("checkForUsername: Returned username didn't match searched");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!profile.uuid) {
|
||||||
|
log.error("checkForUsername: Returned profile didn't include a uuid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
uuid: UUID.cast(profile.uuid),
|
||||||
|
username: profile.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 {
|
function startSettingGroupMetadata(): StartSettingGroupMetadataActionType {
|
||||||
return { type: 'START_SETTING_GROUP_METADATA' };
|
return { type: 'START_SETTING_GROUP_METADATA' };
|
||||||
}
|
}
|
||||||
|
@ -1951,6 +2081,7 @@ export function getEmptyState(): ConversationsStateType {
|
||||||
conversationsByE164: {},
|
conversationsByE164: {},
|
||||||
conversationsByUuid: {},
|
conversationsByUuid: {},
|
||||||
conversationsByGroupId: {},
|
conversationsByGroupId: {},
|
||||||
|
conversationsByUsername: {},
|
||||||
outboundMessagesPendingConversationVerification: {},
|
outboundMessagesPendingConversationVerification: {},
|
||||||
messagesByConversation: {},
|
messagesByConversation: {},
|
||||||
messagesLookup: {},
|
messagesLookup: {},
|
||||||
|
@ -2033,12 +2164,16 @@ export function updateConversationLookups(
|
||||||
state: ConversationsStateType
|
state: ConversationsStateType
|
||||||
): Pick<
|
): Pick<
|
||||||
ConversationsStateType,
|
ConversationsStateType,
|
||||||
'conversationsByE164' | 'conversationsByUuid' | 'conversationsByGroupId'
|
| 'conversationsByE164'
|
||||||
|
| 'conversationsByUuid'
|
||||||
|
| 'conversationsByGroupId'
|
||||||
|
| 'conversationsByUsername'
|
||||||
> {
|
> {
|
||||||
const result = {
|
const result = {
|
||||||
conversationsByE164: state.conversationsByE164,
|
conversationsByE164: state.conversationsByE164,
|
||||||
conversationsByUuid: state.conversationsByUuid,
|
conversationsByUuid: state.conversationsByUuid,
|
||||||
conversationsByGroupId: state.conversationsByGroupId,
|
conversationsByGroupId: state.conversationsByGroupId,
|
||||||
|
conversationsByUsername: state.conversationsByUsername,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (removed && removed.e164) {
|
if (removed && removed.e164) {
|
||||||
|
@ -2053,6 +2188,12 @@ export function updateConversationLookups(
|
||||||
removed.groupId
|
removed.groupId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (removed && removed.username) {
|
||||||
|
result.conversationsByUsername = omit(
|
||||||
|
result.conversationsByUsername,
|
||||||
|
removed.username
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (added && added.e164) {
|
if (added && added.e164) {
|
||||||
result.conversationsByE164 = {
|
result.conversationsByE164 = {
|
||||||
|
@ -2072,6 +2213,12 @@ export function updateConversationLookups(
|
||||||
[added.groupId]: added,
|
[added.groupId]: added,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (added && added.username) {
|
||||||
|
result.conversationsByUsername = {
|
||||||
|
...result.conversationsByUsername,
|
||||||
|
[added.username]: added,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -3045,6 +3192,7 @@ export function reducer(
|
||||||
composer: {
|
composer: {
|
||||||
step: ComposerStep.StartDirectConversation,
|
step: ComposerStep.StartDirectConversation,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isFetchingUsername: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3200,8 +3348,14 @@ export function reducer(
|
||||||
);
|
);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
if (composer?.step === ComposerStep.SetGroupMetadata) {
|
if (
|
||||||
assert(false, 'Setting compose search term at this step is a no-op');
|
composer.step !== ComposerStep.StartDirectConversation &&
|
||||||
|
composer.step !== ComposerStep.ChooseGroupMembers
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
`Setting compose search term at step ${composer.step} is a no-op`
|
||||||
|
);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3214,6 +3368,30 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === 'SET_IS_FETCHING_USERNAME') {
|
||||||
|
const { composer } = state;
|
||||||
|
if (!composer) {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
'Setting compose username 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');
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
const { isFetchingUsername } = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
composer: {
|
||||||
|
...composer,
|
||||||
|
isFetchingUsername,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === COMPOSE_TOGGLE_EDITING_AVATAR) {
|
if (action.type === COMPOSE_TOGGLE_EDITING_AVATAR) {
|
||||||
const { composer } = state;
|
const { composer } = state;
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
export type GlobalModalsStateType = {
|
export type GlobalModalsStateType = {
|
||||||
readonly contactModalState?: ContactModalStateType;
|
readonly contactModalState?: ContactModalStateType;
|
||||||
readonly isProfileEditorVisible: boolean;
|
readonly isProfileEditorVisible: boolean;
|
||||||
|
readonly isWhatsNewVisible: boolean;
|
||||||
readonly profileEditorHasError: boolean;
|
readonly profileEditorHasError: boolean;
|
||||||
readonly safetyNumberModalContactId?: string;
|
readonly safetyNumberModalContactId?: string;
|
||||||
readonly isWhatsNewVisible: boolean;
|
readonly usernameNotFoundModalState?: UsernameNotFoundModalStateType;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
@ -16,6 +17,10 @@ export type GlobalModalsStateType = {
|
||||||
const HIDE_CONTACT_MODAL = 'globalModals/HIDE_CONTACT_MODAL';
|
const HIDE_CONTACT_MODAL = 'globalModals/HIDE_CONTACT_MODAL';
|
||||||
const SHOW_CONTACT_MODAL = 'globalModals/SHOW_CONTACT_MODAL';
|
const SHOW_CONTACT_MODAL = 'globalModals/SHOW_CONTACT_MODAL';
|
||||||
const SHOW_WHATS_NEW_MODAL = 'globalModals/SHOW_WHATS_NEW_MODAL_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 HIDE_WHATS_NEW_MODAL = 'globalModals/HIDE_WHATS_NEW_MODAL_MODAL';
|
const HIDE_WHATS_NEW_MODAL = 'globalModals/HIDE_WHATS_NEW_MODAL_MODAL';
|
||||||
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
|
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
|
||||||
export const TOGGLE_PROFILE_EDITOR_ERROR =
|
export const TOGGLE_PROFILE_EDITOR_ERROR =
|
||||||
|
@ -27,6 +32,10 @@ export type ContactModalStateType = {
|
||||||
conversationId?: string;
|
conversationId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UsernameNotFoundModalStateType = {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
|
||||||
type HideContactModalActionType = {
|
type HideContactModalActionType = {
|
||||||
type: typeof HIDE_CONTACT_MODAL;
|
type: typeof HIDE_CONTACT_MODAL;
|
||||||
};
|
};
|
||||||
|
@ -44,6 +53,17 @@ type ShowWhatsNewModalActionType = {
|
||||||
type: typeof SHOW_WHATS_NEW_MODAL;
|
type: typeof SHOW_WHATS_NEW_MODAL;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type HideUsernameNotFoundModalActionType = {
|
||||||
|
type: typeof HIDE_USERNAME_NOT_FOUND_MODAL;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShowUsernameNotFoundModalActionType = {
|
||||||
|
type: typeof SHOW_USERNAME_NOT_FOUND_MODAL;
|
||||||
|
payload: {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
type ToggleProfileEditorActionType = {
|
type ToggleProfileEditorActionType = {
|
||||||
type: typeof TOGGLE_PROFILE_EDITOR;
|
type: typeof TOGGLE_PROFILE_EDITOR;
|
||||||
};
|
};
|
||||||
|
@ -62,6 +82,8 @@ export type GlobalModalsActionType =
|
||||||
| ShowContactModalActionType
|
| ShowContactModalActionType
|
||||||
| HideWhatsNewModalActionType
|
| HideWhatsNewModalActionType
|
||||||
| ShowWhatsNewModalActionType
|
| ShowWhatsNewModalActionType
|
||||||
|
| HideUsernameNotFoundModalActionType
|
||||||
|
| ShowUsernameNotFoundModalActionType
|
||||||
| ToggleProfileEditorActionType
|
| ToggleProfileEditorActionType
|
||||||
| ToggleProfileEditorErrorActionType
|
| ToggleProfileEditorErrorActionType
|
||||||
| ToggleSafetyNumberModalActionType;
|
| ToggleSafetyNumberModalActionType;
|
||||||
|
@ -73,6 +95,8 @@ export const actions = {
|
||||||
showContactModal,
|
showContactModal,
|
||||||
hideWhatsNewModal,
|
hideWhatsNewModal,
|
||||||
showWhatsNewModal,
|
showWhatsNewModal,
|
||||||
|
hideUsernameNotFoundModal,
|
||||||
|
showUsernameNotFoundModal,
|
||||||
toggleProfileEditor,
|
toggleProfileEditor,
|
||||||
toggleProfileEditorHasError,
|
toggleProfileEditorHasError,
|
||||||
toggleSafetyNumberModal,
|
toggleSafetyNumberModal,
|
||||||
|
@ -109,6 +133,23 @@ function showWhatsNewModal(): ShowWhatsNewModalActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hideUsernameNotFoundModal(): HideUsernameNotFoundModalActionType {
|
||||||
|
return {
|
||||||
|
type: HIDE_USERNAME_NOT_FOUND_MODAL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUsernameNotFoundModal(
|
||||||
|
username: string
|
||||||
|
): ShowUsernameNotFoundModalActionType {
|
||||||
|
return {
|
||||||
|
type: SHOW_USERNAME_NOT_FOUND_MODAL,
|
||||||
|
payload: {
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function toggleProfileEditor(): ToggleProfileEditorActionType {
|
function toggleProfileEditor(): ToggleProfileEditorActionType {
|
||||||
return { type: TOGGLE_PROFILE_EDITOR };
|
return { type: TOGGLE_PROFILE_EDITOR };
|
||||||
}
|
}
|
||||||
|
@ -168,6 +209,24 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === HIDE_USERNAME_NOT_FOUND_MODAL) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
usernameNotFoundModalState: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.type === SHOW_USERNAME_NOT_FOUND_MODAL) {
|
||||||
|
const { username } = action.payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
usernameNotFoundModalState: {
|
||||||
|
username,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === SHOW_CONTACT_MODAL) {
|
if (action.type === SHOW_CONTACT_MODAL) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -107,6 +107,12 @@ export const getConversationsByGroupId = createSelector(
|
||||||
return state.conversationsByGroupId;
|
return state.conversationsByGroupId;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
export const getConversationsByUsername = createSelector(
|
||||||
|
getConversations,
|
||||||
|
(state: ConversationsStateType): ConversationLookupType => {
|
||||||
|
return state.conversationsByUsername;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const getAllConversations = createSelector(
|
const getAllConversations = createSelector(
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
|
@ -397,6 +403,24 @@ export const getComposerConversationSearchTerm = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getIsFetchingUsername = createSelector(
|
||||||
|
getComposerState,
|
||||||
|
(composer): boolean => {
|
||||||
|
if (!composer) {
|
||||||
|
assert(false, 'getIsFetchingUsername: composer is not open');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (composer.step !== ComposerStep.StartDirectConversation) {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
`getIsFetchingUsername: step ${composer.step} has no isFetchingUsername key`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return composer.isFetchingUsername;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function isTrusted(conversation: ConversationType): boolean {
|
function isTrusted(conversation: ConversationType): boolean {
|
||||||
if (conversation.type === 'group') {
|
if (conversation.type === 'group') {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -21,19 +21,23 @@ import {
|
||||||
} from '../selectors/search';
|
} from '../selectors/search';
|
||||||
import { getIntl, getRegionCode, getTheme } from '../selectors/user';
|
import { getIntl, getRegionCode, getTheme } from '../selectors/user';
|
||||||
import { getBadgesById } from '../selectors/badges';
|
import { getBadgesById } from '../selectors/badges';
|
||||||
import { getPreferredLeftPaneWidth } from '../selectors/items';
|
import {
|
||||||
|
getPreferredLeftPaneWidth,
|
||||||
|
getUsernamesEnabled,
|
||||||
|
} from '../selectors/items';
|
||||||
import {
|
import {
|
||||||
getCantAddContactForModal,
|
getCantAddContactForModal,
|
||||||
getComposeAvatarData,
|
getComposeAvatarData,
|
||||||
getComposeGroupAvatar,
|
getComposeGroupAvatar,
|
||||||
getComposeGroupExpireTimer,
|
getComposeGroupExpireTimer,
|
||||||
getComposeGroupName,
|
getComposeGroupName,
|
||||||
getComposeSelectedContacts,
|
|
||||||
getComposerConversationSearchTerm,
|
getComposerConversationSearchTerm,
|
||||||
getComposerStep,
|
getComposerStep,
|
||||||
|
getComposeSelectedContacts,
|
||||||
getFilteredCandidateContactsForNewGroup,
|
getFilteredCandidateContactsForNewGroup,
|
||||||
getFilteredComposeContacts,
|
getFilteredComposeContacts,
|
||||||
getFilteredComposeGroups,
|
getFilteredComposeGroups,
|
||||||
|
getIsFetchingUsername,
|
||||||
getLeftPaneLists,
|
getLeftPaneLists,
|
||||||
getMaximumGroupSizeModalState,
|
getMaximumGroupSizeModalState,
|
||||||
getRecommendedGroupSizeModalState,
|
getRecommendedGroupSizeModalState,
|
||||||
|
@ -126,6 +130,8 @@ const getModeSpecificProps = (
|
||||||
composeGroups: getFilteredComposeGroups(state),
|
composeGroups: getFilteredComposeGroups(state),
|
||||||
regionCode: getRegionCode(state),
|
regionCode: getRegionCode(state),
|
||||||
searchTerm: getComposerConversationSearchTerm(state),
|
searchTerm: getComposerConversationSearchTerm(state),
|
||||||
|
isUsernamesEnabled: getUsernamesEnabled(state),
|
||||||
|
isFetchingUsername: getIsFetchingUsername(state),
|
||||||
};
|
};
|
||||||
case ComposerStep.ChooseGroupMembers:
|
case ComposerStep.ChooseGroupMembers:
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { OneTimeModalState } from '../../groups/toggleSelectedContactForGroupAdd
|
||||||
export const defaultStartDirectConversationComposerState = {
|
export const defaultStartDirectConversationComposerState = {
|
||||||
step: ComposerStep.StartDirectConversation as const,
|
step: ComposerStep.StartDirectConversation as const,
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isFetchingUsername: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultChooseGroupMembersComposerState = {
|
export const defaultChooseGroupMembersComposerState = {
|
||||||
|
|
|
@ -21,6 +21,7 @@ describe('filterAndSortConversationsByTitle', () => {
|
||||||
name: 'Carlos Santana',
|
name: 'Carlos Santana',
|
||||||
title: 'Carlos Santana',
|
title: 'Carlos Santana',
|
||||||
e164: '+16505559876',
|
e164: '+16505559876',
|
||||||
|
username: 'thisismyusername',
|
||||||
}),
|
}),
|
||||||
getDefaultConversation({
|
getDefaultConversation({
|
||||||
name: 'Aaron Aardvark',
|
name: 'Aaron Aardvark',
|
||||||
|
@ -64,6 +65,14 @@ describe('filterAndSortConversationsByTitle', () => {
|
||||||
).map(contact => contact.title);
|
).map(contact => contact.title);
|
||||||
assert.sameMembers(titles, ['Carlos Santana', '+16505551234']);
|
assert.sameMembers(titles, ['Carlos Santana', '+16505551234']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can search for contacts by username', () => {
|
||||||
|
const titles = filterAndSortConversationsByTitle(
|
||||||
|
conversations,
|
||||||
|
'thisis'
|
||||||
|
).map(contact => contact.title);
|
||||||
|
assert.sameMembers(titles, ['Carlos Santana']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filterAndSortConversationsByRecent', () => {
|
describe('filterAndSortConversationsByRecent', () => {
|
||||||
|
|
|
@ -1234,8 +1234,8 @@ describe('both/state/ducks/conversations', () => {
|
||||||
...getEmptyState(),
|
...getEmptyState(),
|
||||||
composer: defaultStartDirectConversationComposerState,
|
composer: defaultStartDirectConversationComposerState,
|
||||||
};
|
};
|
||||||
const action = setComposeSearchTerm('foo bar');
|
|
||||||
const result = reducer(state, action);
|
const result = reducer(state, setComposeSearchTerm('foo bar'));
|
||||||
|
|
||||||
assert.deepEqual(result.composer, {
|
assert.deepEqual(result.composer, {
|
||||||
...defaultStartDirectConversationComposerState,
|
...defaultStartDirectConversationComposerState,
|
||||||
|
|
|
@ -28,6 +28,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(helper.getBackAction({ showInbox }), showInbox);
|
assert.strictEqual(helper.getBackAction({ showInbox }), showInbox);
|
||||||
|
@ -42,6 +44,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
|
@ -54,6 +58,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
4
|
4
|
||||||
);
|
);
|
||||||
|
@ -66,11 +72,41 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
7
|
7
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns the number of contacts, number groups + 4 (for headers and username)', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
new LeftPaneComposeHelper({
|
||||||
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
|
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
||||||
|
regionCode: 'US',
|
||||||
|
searchTerm: 'someone',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
|
}).getRowCount(),
|
||||||
|
8
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('if usernames are disabled, two less rows are shown', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
new LeftPaneComposeHelper({
|
||||||
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
|
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
||||||
|
regionCode: 'US',
|
||||||
|
searchTerm: 'someone',
|
||||||
|
isUsernamesEnabled: false,
|
||||||
|
isFetchingUsername: false,
|
||||||
|
}).getRowCount(),
|
||||||
|
6
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns the number of conversations + the headers, but not for a phone number', () => {
|
it('returns the number of conversations + the headers, but not for a phone number', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneComposeHelper({
|
new LeftPaneComposeHelper({
|
||||||
|
@ -78,8 +114,10 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
0
|
2
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneComposeHelper({
|
new LeftPaneComposeHelper({
|
||||||
|
@ -87,8 +125,10 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
3
|
5
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneComposeHelper({
|
new LeftPaneComposeHelper({
|
||||||
|
@ -96,8 +136,10 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [getDefaultConversation()],
|
composeGroups: [getDefaultConversation()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
5
|
7
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -108,18 +150,36 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '+16505551234',
|
searchTerm: '+16505551234',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the number of contacts + 2 (for the "Start new conversation" button and header) if searching for a phone number', () => {
|
it('returns 2 if just username in results', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
new LeftPaneComposeHelper({
|
||||||
|
composeContacts: [],
|
||||||
|
composeGroups: [],
|
||||||
|
regionCode: 'US',
|
||||||
|
searchTerm: 'someone',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
|
}).getRowCount(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the number of contacts + 4 (for the "Start new conversation" button and header) if searching for a phone number', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneComposeHelper({
|
new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '+16505551234',
|
searchTerm: '+16505551234',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
4
|
4
|
||||||
);
|
);
|
||||||
|
@ -133,6 +193,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(helper.getRow(0), {
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
@ -151,6 +213,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(helper.getRow(0), {
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
@ -184,6 +248,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups,
|
composeGroups,
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(helper.getRow(0), {
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
@ -215,12 +281,14 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns no rows if searching and there are no results', () => {
|
it('returns no rows if searching, no results, and usernames are disabled', () => {
|
||||||
const helper = new LeftPaneComposeHelper({
|
const helper = new LeftPaneComposeHelper({
|
||||||
composeContacts: [],
|
composeContacts: [],
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: false,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isUndefined(helper.getRow(0));
|
assert.isUndefined(helper.getRow(0));
|
||||||
|
@ -237,6 +305,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(helper.getRow(1), {
|
assert.deepEqual(helper.getRow(1), {
|
||||||
|
@ -255,6 +325,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '+16505551234',
|
searchTerm: '+16505551234',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(helper.getRow(0), {
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
@ -264,6 +336,31 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
assert.isUndefined(helper.getRow(1));
|
assert.isUndefined(helper.getRow(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns just a "find by username" header if no results', () => {
|
||||||
|
const username = 'someone';
|
||||||
|
const isFetchingUsername = true;
|
||||||
|
|
||||||
|
const helper = new LeftPaneComposeHelper({
|
||||||
|
composeContacts: [],
|
||||||
|
composeGroups: [],
|
||||||
|
regionCode: 'US',
|
||||||
|
searchTerm: username,
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
type: RowType.Header,
|
||||||
|
i18nKey: 'findByUsernameHeader',
|
||||||
|
});
|
||||||
|
assert.deepEqual(helper.getRow(1), {
|
||||||
|
type: RowType.UsernameSearchResult,
|
||||||
|
username,
|
||||||
|
isFetchingUsername,
|
||||||
|
});
|
||||||
|
assert.isUndefined(helper.getRow(2));
|
||||||
|
});
|
||||||
|
|
||||||
it('returns a "start new conversation" row, a header, and contacts if searching for a phone number', () => {
|
it('returns a "start new conversation" row, a header, and contacts if searching for a phone number', () => {
|
||||||
const composeContacts = [
|
const composeContacts = [
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
|
@ -274,6 +371,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '+16505551234',
|
searchTerm: '+16505551234',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(helper.getRow(0), {
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
@ -302,6 +401,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isUndefined(helper.getConversationAndMessageAtIndex(0));
|
assert.isUndefined(helper.getConversationAndMessageAtIndex(0));
|
||||||
|
@ -315,6 +416,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isUndefined(
|
assert.isUndefined(
|
||||||
|
@ -328,42 +431,46 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('shouldRecomputeRowHeights', () => {
|
describe('shouldRecomputeRowHeights', () => {
|
||||||
it('returns false if going from "no header" to "no header"', () => {
|
it('returns false if just search changes, so "Find by username" header is in same position', () => {
|
||||||
const helper = new LeftPaneComposeHelper({
|
const helper = new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [],
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
helper.shouldRecomputeRowHeights({
|
helper.shouldRecomputeRowHeights({
|
||||||
composeContacts: [getDefaultConversation()],
|
composeContacts: [],
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'different search',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
helper.shouldRecomputeRowHeights({
|
helper.shouldRecomputeRowHeights({
|
||||||
composeContacts: [
|
composeContacts: [],
|
||||||
getDefaultConversation(),
|
|
||||||
getDefaultConversation(),
|
|
||||||
getDefaultConversation(),
|
|
||||||
],
|
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'bing bong',
|
searchTerm: 'last search',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false if going from "has header" to "has header"', () => {
|
it('returns true if "Find by usernames" header changes location or goes away', () => {
|
||||||
const helper = new LeftPaneComposeHelper({
|
const helper = new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
|
@ -372,6 +479,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
|
@ -380,16 +489,20 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '+16505559876',
|
searchTerm: '+16505559876',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true if going from "no header" to "has header"', () => {
|
it('returns true if search changes or becomes an e164', () => {
|
||||||
const helper = new LeftPaneComposeHelper({
|
const helper = new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
|
@ -398,6 +511,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
|
@ -406,16 +521,20 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '+16505551234',
|
searchTerm: '+16505551234',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true if going from "has header" to "no header"', () => {
|
it('returns true if going from no search to some search (showing "Find by username" section)', () => {
|
||||||
const helper = new LeftPaneComposeHelper({
|
const helper = new LeftPaneComposeHelper({
|
||||||
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
composeContacts: [getDefaultConversation(), getDefaultConversation()],
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: '',
|
searchTerm: '',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
|
@ -424,6 +543,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -434,6 +555,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
|
@ -442,6 +565,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -450,6 +575,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
|
@ -458,6 +585,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [],
|
composeGroups: [],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -468,6 +597,8 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeGroups: [getDefaultConversation()],
|
composeGroups: [getDefaultConversation()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'soup',
|
searchTerm: 'soup',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
|
@ -475,7 +606,9 @@ describe('LeftPaneComposeHelper', () => {
|
||||||
composeContacts: [getDefaultConversation()],
|
composeContacts: [getDefaultConversation()],
|
||||||
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
composeGroups: [getDefaultConversation(), getDefaultConversation()],
|
||||||
regionCode: 'US',
|
regionCode: 'US',
|
||||||
searchTerm: 'sandwich',
|
searchTerm: 'soup',
|
||||||
|
isUsernamesEnabled: true,
|
||||||
|
isFetchingUsername: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import * as Username from '../../types/Username';
|
||||||
|
|
||||||
|
describe('Username', () => {
|
||||||
|
describe('getUsernameFromSearch', () => {
|
||||||
|
const { getUsernameFromSearch } = Username;
|
||||||
|
|
||||||
|
it('matches invalid username searches', () => {
|
||||||
|
assert.strictEqual(getUsernameFromSearch('username!'), 'username!');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('1username'), '1username');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('us'), 'us');
|
||||||
|
assert.strictEqual(
|
||||||
|
getUsernameFromSearch('username901234567890123456'),
|
||||||
|
'username901234567890123456'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches valid username searches', () => {
|
||||||
|
assert.strictEqual(getUsernameFromSearch('username_34'), 'username_34');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('u5ername'), 'u5ername');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('use'), 'use');
|
||||||
|
assert.strictEqual(
|
||||||
|
getUsernameFromSearch('username90123456789012345'),
|
||||||
|
'username90123456789012345'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches valid and invalid usernames with @ prefix', () => {
|
||||||
|
assert.strictEqual(getUsernameFromSearch('@username!'), 'username!');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('@1username'), '1username');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('@username_34'), 'username_34');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('@u5ername'), 'u5ername');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches valid and invalid usernames with @ suffix', () => {
|
||||||
|
assert.strictEqual(getUsernameFromSearch('username!@'), 'username!');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('1username@'), '1username');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('username_34@'), 'username_34');
|
||||||
|
assert.strictEqual(getUsernameFromSearch('u5ername@'), 'u5ername');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not match something that looks like a phone number', () => {
|
||||||
|
assert.isUndefined(getUsernameFromSearch('+'));
|
||||||
|
assert.isUndefined(getUsernameFromSearch('2223'));
|
||||||
|
assert.isUndefined(getUsernameFromSearch('+3'));
|
||||||
|
assert.isUndefined(getUsernameFromSearch('+234234234233'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isValidUsername', () => {
|
||||||
|
const { isValidUsername } = Username;
|
||||||
|
|
||||||
|
it('does not match invalid username searches', () => {
|
||||||
|
assert.isFalse(isValidUsername('username!'));
|
||||||
|
assert.isFalse(isValidUsername('1username'));
|
||||||
|
assert.isFalse(isValidUsername('us'));
|
||||||
|
assert.isFalse(isValidUsername('username901234567890123456'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches valid usernames', () => {
|
||||||
|
assert.isTrue(isValidUsername('username_34'));
|
||||||
|
assert.isTrue(isValidUsername('u5ername'));
|
||||||
|
assert.isTrue(isValidUsername('use'));
|
||||||
|
assert.isTrue(isValidUsername('username90123456789012345'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not match valid and invalid usernames with @ prefix or suffix', () => {
|
||||||
|
assert.isFalse(isValidUsername('@username_34'));
|
||||||
|
assert.isFalse(isValidUsername('@1username'));
|
||||||
|
assert.isFalse(isValidUsername('username_34@'));
|
||||||
|
assert.isFalse(isValidUsername('1username@'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -2043,7 +2043,7 @@ export default class MessageSender {
|
||||||
profileKeyCredentialRequest?: string;
|
profileKeyCredentialRequest?: string;
|
||||||
userLanguages: ReadonlyArray<string>;
|
userLanguages: ReadonlyArray<string>;
|
||||||
}>
|
}>
|
||||||
): Promise<ReturnType<WebAPIType['getProfile']>> {
|
): ReturnType<WebAPIType['getProfile']> {
|
||||||
const { accessKey } = options;
|
const { accessKey } = options;
|
||||||
|
|
||||||
if (accessKey) {
|
if (accessKey) {
|
||||||
|
@ -2057,6 +2057,12 @@ export default class MessageSender {
|
||||||
return this.server.getProfile(number, options);
|
return this.server.getProfile(number, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProfileForUsername(
|
||||||
|
username: string
|
||||||
|
): ReturnType<WebAPIType['getProfileForUsername']> {
|
||||||
|
return this.server.getProfileForUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
async getUuidsForE164s(
|
async getUuidsForE164s(
|
||||||
numbers: ReadonlyArray<string>
|
numbers: ReadonlyArray<string>
|
||||||
): Promise<Dictionary<UUIDStringType | null>> {
|
): Promise<Dictionary<UUIDStringType | null>> {
|
||||||
|
|
|
@ -770,6 +770,7 @@ export type WebAPIType = {
|
||||||
userLanguages: ReadonlyArray<string>;
|
userLanguages: ReadonlyArray<string>;
|
||||||
}
|
}
|
||||||
) => Promise<ProfileType>;
|
) => Promise<ProfileType>;
|
||||||
|
getProfileForUsername: (username: string) => Promise<ProfileType>;
|
||||||
getProfileUnauth: (
|
getProfileUnauth: (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
options: {
|
options: {
|
||||||
|
@ -1077,6 +1078,7 @@ export function initialize({
|
||||||
getKeysForIdentifierUnauth,
|
getKeysForIdentifierUnauth,
|
||||||
getMyKeys,
|
getMyKeys,
|
||||||
getProfile,
|
getProfile,
|
||||||
|
getProfileForUsername,
|
||||||
getProfileUnauth,
|
getProfileUnauth,
|
||||||
getBadgeImageFile,
|
getBadgeImageFile,
|
||||||
getProvisioningResource,
|
getProvisioningResource,
|
||||||
|
@ -1385,6 +1387,12 @@ export function initialize({
|
||||||
})) as ProfileType;
|
})) as ProfileType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getProfileForUsername(usernameToFetch: string) {
|
||||||
|
return getProfile(`username/${usernameToFetch}`, {
|
||||||
|
userLanguages: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function putProfile(
|
async function putProfile(
|
||||||
jsonData: ProfileRequestDataType
|
jsonData: ProfileRequestDataType
|
||||||
): Promise<UploadAvatarHeadersType | undefined> {
|
): Promise<UploadAvatarHeadersType | undefined> {
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export function isValidUsername(searchTerm: string): boolean {
|
||||||
|
return /^[a-z][0-9a-z_]{2,24}$/.test(searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUsernameFromSearch(searchTerm: string): string | undefined {
|
||||||
|
if (/^[+0-9]+$/.test(searchTerm)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const match = /^@?(.*?)@?$/.exec(searchTerm);
|
||||||
|
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
|
@ -24,6 +24,10 @@ const FUSE_OPTIONS: FuseOptions<ConversationType> = {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
weight: 1,
|
weight: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'username',
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'e164',
|
name: 'e164',
|
||||||
weight: 0.5,
|
weight: 0.5,
|
||||||
|
|
|
@ -115,19 +115,26 @@ Whisper.InboxView = Whisper.View.extend({
|
||||||
this.conversation_stack.unload();
|
this.conversation_stack.unload();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.Whisper.events.on('showConversation', async (id, messageId) => {
|
window.Whisper.events.on(
|
||||||
const conversation =
|
'showConversation',
|
||||||
await window.ConversationController.getOrCreateAndWait(id, 'private');
|
async (id, messageId, username) => {
|
||||||
|
const conversation =
|
||||||
|
await window.ConversationController.getOrCreateAndWait(
|
||||||
|
id,
|
||||||
|
'private',
|
||||||
|
{ username }
|
||||||
|
);
|
||||||
|
|
||||||
conversation.setMarkedUnread(false);
|
conversation.setMarkedUnread(false);
|
||||||
|
|
||||||
const { openConversationExternal } = window.reduxActions.conversations;
|
const { openConversationExternal } = window.reduxActions.conversations;
|
||||||
if (openConversationExternal) {
|
if (openConversationExternal) {
|
||||||
openConversationExternal(conversation.id, messageId);
|
openConversationExternal(conversation.id, messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.conversation_stack.open(conversation, messageId);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
this.conversation_stack.open(conversation, messageId);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.Whisper.events.on('loadingProgress', count => {
|
window.Whisper.events.on('loadingProgress', count => {
|
||||||
const view = this.appLoadingScreen;
|
const view = this.appLoadingScreen;
|
||||||
|
|
Loading…
Reference in New Issue