Various search UI improvements
This commit is contained in:
parent
630394d91d
commit
a9cb621eb6
25 changed files with 835 additions and 577 deletions
|
@ -1 +0,0 @@
|
||||||
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m12 1a11 11 0 1 0 11 11 11 11 0 0 0 -11-11zm0 1.5a9.485 9.485 0 0 1 7.15 15.735 5.966 5.966 0 0 0 -4.65-2.235h-5a5.966 5.966 0 0 0 -4.65 2.235 9.485 9.485 0 0 1 7.15-15.735zm4 6.971c0 2.623-1.791 5.029-4 5.029s-4-2.406-4-5.029a4.16 4.16 0 0 1 4-4.471 4.16 4.16 0 0 1 4 4.471z"/></svg>
|
|
Before Width: | Height: | Size: 376 B |
|
@ -2699,158 +2699,6 @@ button.ConversationDetails__action-button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__search {
|
|
||||||
flex-grow: 1;
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
|
|
||||||
&__input {
|
|
||||||
width: 100%;
|
|
||||||
height: 28px;
|
|
||||||
|
|
||||||
padding-left: 30px;
|
|
||||||
padding-right: 5px;
|
|
||||||
|
|
||||||
border-radius: 14px;
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
@include font-body-2;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-05;
|
|
||||||
color: $color-gray-90;
|
|
||||||
border: solid 1px $color-gray-02;
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
color: $color-gray-05;
|
|
||||||
background-color: $color-gray-95;
|
|
||||||
border: solid 1px $color-gray-80;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:placeholder {
|
|
||||||
color: $color-gray-45;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
border: solid 1px $color-ultramarine;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--with-text {
|
|
||||||
padding-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--in-conversation {
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
position: absolute;
|
|
||||||
left: 8px;
|
|
||||||
top: 6px;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
|
|
||||||
cursor: text;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-75);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__in-conversation-pill {
|
|
||||||
position: absolute;
|
|
||||||
left: 3px;
|
|
||||||
top: 3px;
|
|
||||||
bottom: 3px;
|
|
||||||
|
|
||||||
border-radius: 14px;
|
|
||||||
width: 42px;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
// Overriding some default button styling
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
outline: none;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
background-color: $color-gray-15;
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
background-color: $color-gray-75;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__avatar-container {
|
|
||||||
margin-left: 4px;
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
background-color: $color-ultramarine;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__avatar {
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg(
|
|
||||||
'../images/icons/v2/profile-circle-outline-24.svg',
|
|
||||||
$color-white
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg(
|
|
||||||
'../images/icons/v2/profile-circle-solid-24.svg',
|
|
||||||
$color-gray-05
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__x-button {
|
|
||||||
margin-left: 2px;
|
|
||||||
|
|
||||||
height: 16px;
|
|
||||||
width: 16px;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__cancel-icon {
|
|
||||||
position: absolute;
|
|
||||||
right: 8px;
|
|
||||||
top: 5px;
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
|
|
||||||
@include light-theme {
|
|
||||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
|
|
||||||
}
|
|
||||||
@include dark-theme {
|
|
||||||
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-left-pane--width-narrow & {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__compose-icon {
|
&__compose-icon {
|
||||||
$icon: '../images/icons/v2/compose-outline-24.svg';
|
$icon: '../images/icons/v2/compose-outline-24.svg';
|
||||||
|
|
||||||
|
@ -5712,6 +5560,8 @@ button.module-image__border-overlay:focus {
|
||||||
|
|
||||||
&__text {
|
&__text {
|
||||||
@include font-body-1-bold;
|
@include font-body-1-bold;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-right: 16px;
|
||||||
|
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
color: $color-gray-90;
|
color: $color-gray-90;
|
||||||
|
|
131
stylesheets/components/LeftPaneSearchInput.scss
Normal file
131
stylesheets/components/LeftPaneSearchInput.scss
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.LeftPaneSearchInput {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
@include font-body-2;
|
||||||
|
@include rounded-corners;
|
||||||
|
border: none;
|
||||||
|
height: 28px;
|
||||||
|
padding-left: 30px;
|
||||||
|
padding-right: 5px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-05;
|
||||||
|
color: $color-gray-90;
|
||||||
|
border: solid 1px $color-gray-02;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-05;
|
||||||
|
background-color: $color-gray-95;
|
||||||
|
border: solid 1px $color-gray-80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:placeholder {
|
||||||
|
color: $color-gray-45;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: solid 1px $color-ultramarine;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--with-text {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--in-conversation {
|
||||||
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
height: 16px;
|
||||||
|
left: 8px;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
width: 16px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-75);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/icons/v2/search-16.svg', $color-gray-25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__in-conversation-pill {
|
||||||
|
@include button-reset;
|
||||||
|
@include rounded-corners;
|
||||||
|
align-items: center;
|
||||||
|
bottom: 3px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
left: 3px;
|
||||||
|
padding: 1px 3px 1px 4px;
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-gray-15;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-gray-75;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__x-button {
|
||||||
|
height: 16px;
|
||||||
|
margin-left: 2px;
|
||||||
|
width: 16px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: $color-ultramarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
background: $color-ultramarine-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__cancel {
|
||||||
|
height: 18px;
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 5px;
|
||||||
|
width: 18px;
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-60);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg('../images/icons/v2/x-24.svg', $color-gray-25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-left-pane--width-narrow & {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -66,6 +66,7 @@
|
||||||
@import './components/IncomingCallBar.scss';
|
@import './components/IncomingCallBar.scss';
|
||||||
@import './components/Input.scss';
|
@import './components/Input.scss';
|
||||||
@import './components/LeftPaneDialog.scss';
|
@import './components/LeftPaneDialog.scss';
|
||||||
|
@import './components/LeftPaneSearchInput.scss';
|
||||||
@import './components/Lightbox.scss';
|
@import './components/Lightbox.scss';
|
||||||
@import './components/MediaQualitySelector.scss';
|
@import './components/MediaQualitySelector.scss';
|
||||||
@import './components/MessageAudio.scss';
|
@import './components/MessageAudio.scss';
|
||||||
|
|
|
@ -1149,7 +1149,7 @@ export async function startApp(): Promise<void> {
|
||||||
document.querySelector(
|
document.querySelector(
|
||||||
'.module-left-pane__header__contents__back-button'
|
'.module-left-pane__header__contents__back-button'
|
||||||
),
|
),
|
||||||
document.querySelector('.module-main-header__search__input'),
|
document.querySelector('.LeftPaneSearchInput__input'),
|
||||||
document.querySelector('.module-main-header__compose-icon'),
|
document.querySelector('.module-main-header__compose-icon'),
|
||||||
document.querySelector(
|
document.querySelector(
|
||||||
'.module-left-pane__compose-search-form__input'
|
'.module-left-pane__compose-search-form__input'
|
||||||
|
@ -1228,8 +1228,8 @@ export async function startApp(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MainHeader search box
|
// Search box
|
||||||
if (className.includes('module-main-header__search__input')) {
|
if (className.includes('LeftPaneSearchInput__input')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export enum AvatarBlur {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AvatarSize {
|
export enum AvatarSize {
|
||||||
|
SIXTEEN = 16,
|
||||||
TWENTY_EIGHT = 28,
|
TWENTY_EIGHT = 28,
|
||||||
THIRTY_TWO = 32,
|
THIRTY_TWO = 32,
|
||||||
THIRTY_SIX = 36,
|
THIRTY_SIX = 36,
|
||||||
|
|
|
@ -83,6 +83,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
cantAddContactToGroup: action('cantAddContactToGroup'),
|
cantAddContactToGroup: action('cantAddContactToGroup'),
|
||||||
canResizeLeftPane: true,
|
canResizeLeftPane: true,
|
||||||
clearGroupCreationError: action('clearGroupCreationError'),
|
clearGroupCreationError: action('clearGroupCreationError'),
|
||||||
|
clearSearch: action('clearSearch'),
|
||||||
closeCantAddContactToGroupModal: action('closeCantAddContactToGroupModal'),
|
closeCantAddContactToGroupModal: action('closeCantAddContactToGroupModal'),
|
||||||
closeMaximumGroupSizeModal: action('closeMaximumGroupSizeModal'),
|
closeMaximumGroupSizeModal: action('closeMaximumGroupSizeModal'),
|
||||||
closeRecommendedGroupSizeModal: action('closeRecommendedGroupSizeModal'),
|
closeRecommendedGroupSizeModal: action('closeRecommendedGroupSizeModal'),
|
||||||
|
@ -131,6 +132,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
selectedConversationId: undefined,
|
selectedConversationId: undefined,
|
||||||
selectedMessageId: undefined,
|
selectedMessageId: undefined,
|
||||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||||
|
searchInConversation: action('searchInConversation'),
|
||||||
setComposeSearchTerm: action('setComposeSearchTerm'),
|
setComposeSearchTerm: action('setComposeSearchTerm'),
|
||||||
setComposeGroupAvatar: action('setComposeGroupAvatar'),
|
setComposeGroupAvatar: action('setComposeGroupAvatar'),
|
||||||
setComposeGroupName: action('setComposeGroupName'),
|
setComposeGroupName: action('setComposeGroupName'),
|
||||||
|
@ -142,11 +144,13 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
startNewConversationFromPhoneNumber: action(
|
startNewConversationFromPhoneNumber: action(
|
||||||
'startNewConversationFromPhoneNumber'
|
'startNewConversationFromPhoneNumber'
|
||||||
),
|
),
|
||||||
|
startSearch: action('startSearch'),
|
||||||
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
||||||
toggleComposeEditingAvatar: action('toggleComposeEditingAvatar'),
|
toggleComposeEditingAvatar: action('toggleComposeEditingAvatar'),
|
||||||
toggleConversationInChooseMembers: action(
|
toggleConversationInChooseMembers: action(
|
||||||
'toggleConversationInChooseMembers'
|
'toggleConversationInChooseMembers'
|
||||||
),
|
),
|
||||||
|
updateSearchTerm: action('updateSearchTerm'),
|
||||||
|
|
||||||
...overrideProps,
|
...overrideProps,
|
||||||
});
|
});
|
||||||
|
@ -393,6 +397,8 @@ story.add('Archive: no archived conversations', () => (
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Archive,
|
mode: LeftPaneMode.Archive,
|
||||||
archivedConversations: [],
|
archivedConversations: [],
|
||||||
|
searchConversation: undefined,
|
||||||
|
searchTerm: '',
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -404,6 +410,25 @@ story.add('Archive: archived conversations', () => (
|
||||||
modeSpecificProps: {
|
modeSpecificProps: {
|
||||||
mode: LeftPaneMode.Archive,
|
mode: LeftPaneMode.Archive,
|
||||||
archivedConversations: defaultConversations,
|
archivedConversations: defaultConversations,
|
||||||
|
searchConversation: undefined,
|
||||||
|
searchTerm: '',
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
story.add('Archive: searching a conversation', () => (
|
||||||
|
<LeftPane
|
||||||
|
{...createProps({
|
||||||
|
modeSpecificProps: {
|
||||||
|
mode: LeftPaneMode.Archive,
|
||||||
|
archivedConversations: defaultConversations,
|
||||||
|
searchConversation: defaultConversations[0],
|
||||||
|
searchTerm: 'foo bar',
|
||||||
|
conversationResults: { isLoading: true },
|
||||||
|
contactResults: { isLoading: true },
|
||||||
|
messageResults: { isLoading: true },
|
||||||
|
primarySendsSms: false,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -94,6 +94,7 @@ export type PropsType = {
|
||||||
// Action Creators
|
// Action Creators
|
||||||
cantAddContactToGroup: (conversationId: string) => void;
|
cantAddContactToGroup: (conversationId: string) => void;
|
||||||
clearGroupCreationError: () => void;
|
clearGroupCreationError: () => void;
|
||||||
|
clearSearch: () => void;
|
||||||
closeCantAddContactToGroupModal: () => void;
|
closeCantAddContactToGroupModal: () => void;
|
||||||
closeMaximumGroupSizeModal: () => void;
|
closeMaximumGroupSizeModal: () => void;
|
||||||
closeRecommendedGroupSizeModal: () => void;
|
closeRecommendedGroupSizeModal: () => void;
|
||||||
|
@ -105,6 +106,7 @@ export type PropsType = {
|
||||||
switchToAssociatedView?: boolean;
|
switchToAssociatedView?: boolean;
|
||||||
}) => void;
|
}) => void;
|
||||||
savePreferredLeftPaneWidth: (_: number) => void;
|
savePreferredLeftPaneWidth: (_: number) => void;
|
||||||
|
searchInConversation: (conversationId: string) => unknown;
|
||||||
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
||||||
setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
|
setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
|
||||||
setComposeGroupName: (_: string) => void;
|
setComposeGroupName: (_: string) => void;
|
||||||
|
@ -112,6 +114,7 @@ export type PropsType = {
|
||||||
showArchivedConversations: () => void;
|
showArchivedConversations: () => void;
|
||||||
showInbox: () => void;
|
showInbox: () => void;
|
||||||
startComposing: () => void;
|
startComposing: () => void;
|
||||||
|
startSearch: () => unknown;
|
||||||
showChooseGroupMembers: () => void;
|
showChooseGroupMembers: () => void;
|
||||||
startSettingGroupMetadata: () => void;
|
startSettingGroupMetadata: () => void;
|
||||||
toggleConversationInChooseMembers: (conversationId: string) => void;
|
toggleConversationInChooseMembers: (conversationId: string) => void;
|
||||||
|
@ -119,6 +122,7 @@ export type PropsType = {
|
||||||
composeReplaceAvatar: ReplaceAvatarActionType;
|
composeReplaceAvatar: ReplaceAvatarActionType;
|
||||||
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
|
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||||
toggleComposeEditingAvatar: () => unknown;
|
toggleComposeEditingAvatar: () => unknown;
|
||||||
|
updateSearchTerm: (_: string) => void;
|
||||||
|
|
||||||
// Render Props
|
// Render Props
|
||||||
renderExpiredBuildDialog: (
|
renderExpiredBuildDialog: (
|
||||||
|
@ -143,6 +147,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
canResizeLeftPane,
|
canResizeLeftPane,
|
||||||
challengeStatus,
|
challengeStatus,
|
||||||
clearGroupCreationError,
|
clearGroupCreationError,
|
||||||
|
clearSearch,
|
||||||
closeCantAddContactToGroupModal,
|
closeCantAddContactToGroupModal,
|
||||||
closeMaximumGroupSizeModal,
|
closeMaximumGroupSizeModal,
|
||||||
closeRecommendedGroupSizeModal,
|
closeRecommendedGroupSizeModal,
|
||||||
|
@ -162,6 +167,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
renderRelinkDialog,
|
renderRelinkDialog,
|
||||||
renderUpdateDialog,
|
renderUpdateDialog,
|
||||||
savePreferredLeftPaneWidth,
|
savePreferredLeftPaneWidth,
|
||||||
|
searchInConversation,
|
||||||
selectedConversationId,
|
selectedConversationId,
|
||||||
selectedMessageId,
|
selectedMessageId,
|
||||||
setChallengeStatus,
|
setChallengeStatus,
|
||||||
|
@ -173,10 +179,12 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
showChooseGroupMembers,
|
showChooseGroupMembers,
|
||||||
showInbox,
|
showInbox,
|
||||||
startComposing,
|
startComposing,
|
||||||
|
startSearch,
|
||||||
startNewConversationFromPhoneNumber,
|
startNewConversationFromPhoneNumber,
|
||||||
startSettingGroupMetadata,
|
startSettingGroupMetadata,
|
||||||
toggleComposeEditingAvatar,
|
toggleComposeEditingAvatar,
|
||||||
toggleConversationInChooseMembers,
|
toggleConversationInChooseMembers,
|
||||||
|
updateSearchTerm,
|
||||||
}) => {
|
}) => {
|
||||||
const [preferredWidth, setPreferredWidth] = useState(
|
const [preferredWidth, setPreferredWidth] = useState(
|
||||||
// This clamp is present just in case we get a bogus value from storage.
|
// This clamp is present just in case we get a bogus value from storage.
|
||||||
|
@ -354,6 +362,12 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
helper.onKeyDown(event, {
|
||||||
|
searchInConversation,
|
||||||
|
selectedConversationId,
|
||||||
|
startSearch,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
@ -363,11 +377,13 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
}, [
|
}, [
|
||||||
helper,
|
helper,
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
|
searchInConversation,
|
||||||
selectedConversationId,
|
selectedConversationId,
|
||||||
selectedMessageId,
|
selectedMessageId,
|
||||||
showChooseGroupMembers,
|
showChooseGroupMembers,
|
||||||
showInbox,
|
showInbox,
|
||||||
startComposing,
|
startComposing,
|
||||||
|
startSearch,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const requiresFullWidth = helper.requiresFullWidth();
|
const requiresFullWidth = helper.requiresFullWidth();
|
||||||
|
@ -524,10 +540,12 @@ export const LeftPane: React.FC<PropsType> = ({
|
||||||
{/* eslint-enable jsx-a11y/no-static-element-interactions */}
|
{/* eslint-enable jsx-a11y/no-static-element-interactions */}
|
||||||
<div className="module-left-pane__header">
|
<div className="module-left-pane__header">
|
||||||
{helper.getHeaderContents({
|
{helper.getHeaderContents({
|
||||||
|
clearSearch,
|
||||||
i18n,
|
i18n,
|
||||||
showInbox,
|
showInbox,
|
||||||
startComposing,
|
startComposing,
|
||||||
showChooseGroupMembers,
|
showChooseGroupMembers,
|
||||||
|
updateSearchTerm,
|
||||||
}) || renderMainHeader()}
|
}) || renderMainHeader()}
|
||||||
</div>
|
</div>
|
||||||
{renderExpiredBuildDialog({ containerWidthBreakpoint: widthBreakpoint })}
|
{renderExpiredBuildDialog({ containerWidthBreakpoint: widthBreakpoint })}
|
||||||
|
|
127
ts/components/LeftPaneSearchInput.tsx
Normal file
127
ts/components/LeftPaneSearchInput.tsx
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { FocusEventHandler } from 'react';
|
||||||
|
import React, { forwardRef, useRef } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { refMerger } from '../util/refMerger';
|
||||||
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
|
|
||||||
|
type PropsType = {
|
||||||
|
disabled?: boolean;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||||
|
onChangeValue: (newValue: string) => unknown;
|
||||||
|
onClear: () => unknown;
|
||||||
|
searchConversation?: ConversationType;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LeftPaneSearchInput = forwardRef<HTMLInputElement, PropsType>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
disabled,
|
||||||
|
i18n,
|
||||||
|
onBlur,
|
||||||
|
onChangeValue,
|
||||||
|
onClear,
|
||||||
|
searchConversation,
|
||||||
|
value,
|
||||||
|
},
|
||||||
|
outerRef
|
||||||
|
) => {
|
||||||
|
const inputRef = useRef<null | HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const emptyOrClear =
|
||||||
|
searchConversation && value ? () => onChangeValue('') : onClear;
|
||||||
|
|
||||||
|
const label = searchConversation
|
||||||
|
? i18n('searchIn', [
|
||||||
|
searchConversation.isMe
|
||||||
|
? i18n('noteToSelf')
|
||||||
|
: searchConversation.title,
|
||||||
|
])
|
||||||
|
: i18n('search');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="LeftPaneSearchInput">
|
||||||
|
{searchConversation ? (
|
||||||
|
// Clicking the non-X part of the pill should focus the input but have a normal
|
||||||
|
// cursor. This effectively simulates `pointer-events: none` while still
|
||||||
|
// letting us change the cursor.
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
|
<div
|
||||||
|
className="LeftPaneSearchInput__in-conversation-pill"
|
||||||
|
onClick={() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={searchConversation.acceptedMessageRequest}
|
||||||
|
avatarPath={searchConversation.avatarPath}
|
||||||
|
color={searchConversation.color}
|
||||||
|
conversationType={searchConversation.type}
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={searchConversation.isMe}
|
||||||
|
noteToSelf={searchConversation.isMe}
|
||||||
|
sharedGroupNames={searchConversation.sharedGroupNames}
|
||||||
|
size={AvatarSize.SIXTEEN}
|
||||||
|
title={searchConversation.title}
|
||||||
|
unblurredAvatarPath={searchConversation.unblurredAvatarPath}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label={i18n('clearSearch')}
|
||||||
|
className="LeftPaneSearchInput__in-conversation-pill__x-button"
|
||||||
|
onClick={onClear}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="LeftPaneSearchInput__icon" />
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
aria-label={label}
|
||||||
|
className={classNames(
|
||||||
|
'LeftPaneSearchInput__input',
|
||||||
|
value && 'LeftPaneSearchInput__input--with-text',
|
||||||
|
searchConversation && 'LeftPaneSearchInput__input--in-conversation'
|
||||||
|
)}
|
||||||
|
dir="auto"
|
||||||
|
disabled={disabled}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={event => {
|
||||||
|
onChangeValue(event.currentTarget.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={event => {
|
||||||
|
const { ctrlKey, key } = event;
|
||||||
|
|
||||||
|
// On Linux, this key combo selects all text.
|
||||||
|
if (window.platform === 'linux' && ctrlKey && key === '/') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else if (key === 'Escape') {
|
||||||
|
emptyOrClear();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder={label}
|
||||||
|
ref={refMerger(inputRef, outerRef)}
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
{value && (
|
||||||
|
<button
|
||||||
|
aria-label={i18n('cancel')}
|
||||||
|
className="LeftPaneSearchInput__cancel"
|
||||||
|
onClick={emptyOrClear}
|
||||||
|
tabIndex={-1}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
|
@ -3,13 +3,14 @@
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
import { text, withKnobs } from '@storybook/addon-knobs';
|
import { text } from '@storybook/addon-knobs';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import { setupI18n } from '../util/setupI18n';
|
import { setupI18n } from '../util/setupI18n';
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
import type { PropsType } from './MainHeader';
|
import type { PropsType } from './MainHeader';
|
||||||
import { MainHeader } from './MainHeader';
|
import { MainHeader } from './MainHeader';
|
||||||
|
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -20,28 +21,12 @@ const requiredText = (name: string, value: string | undefined) =>
|
||||||
const optionalText = (name: string, value: string | undefined) =>
|
const optionalText = (name: string, value: string | undefined) =>
|
||||||
text(name, value || '') || undefined;
|
text(name, value || '') || undefined;
|
||||||
|
|
||||||
// Storybook types are incorrect
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
story.addDecorator((withKnobs as any)({ escapeHTML: false }));
|
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
searchTerm: requiredText('searchTerm', overrideProps.searchTerm),
|
searchTerm: requiredText('searchTerm', overrideProps.searchTerm),
|
||||||
searchConversationName: optionalText(
|
searchConversation: overrideProps.searchConversation,
|
||||||
'searchConversationName',
|
|
||||||
overrideProps.searchConversationName
|
|
||||||
),
|
|
||||||
searchConversationId: optionalText(
|
|
||||||
'searchConversationId',
|
|
||||||
overrideProps.searchConversationId
|
|
||||||
),
|
|
||||||
selectedConversation: undefined,
|
selectedConversation: undefined,
|
||||||
startSearchCounter: 0,
|
startSearchCounter: 0,
|
||||||
|
|
||||||
ourConversationId: '',
|
|
||||||
ourUuid: '',
|
|
||||||
ourNumber: '',
|
|
||||||
regionCode: '',
|
|
||||||
|
|
||||||
phoneNumber: optionalText('phoneNumber', overrideProps.phoneNumber),
|
phoneNumber: optionalText('phoneNumber', overrideProps.phoneNumber),
|
||||||
title: requiredText('title', overrideProps.title),
|
title: requiredText('title', overrideProps.title),
|
||||||
name: optionalText('name', overrideProps.name),
|
name: optionalText('name', overrideProps.name),
|
||||||
|
@ -51,10 +36,6 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
i18n,
|
i18n,
|
||||||
|
|
||||||
updateSearchTerm: action('updateSearchTerm'),
|
updateSearchTerm: action('updateSearchTerm'),
|
||||||
searchMessages: action('searchMessages'),
|
|
||||||
searchDiscussions: action('searchDiscussions'),
|
|
||||||
startSearch: action('startSearch'),
|
|
||||||
searchInConversation: action('searchInConversation'),
|
|
||||||
clearConversationSearch: action('clearConversationSearch'),
|
clearConversationSearch: action('clearConversationSearch'),
|
||||||
clearSearch: action('clearSearch'),
|
clearSearch: action('clearSearch'),
|
||||||
startUpdate: action('startUpdate'),
|
startUpdate: action('startUpdate'),
|
||||||
|
@ -101,8 +82,7 @@ story.add('Search Term', () => {
|
||||||
story.add('Searching Conversation', () => {
|
story.add('Searching Conversation', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
name: 'John Smith',
|
name: 'John Smith',
|
||||||
searchConversationId: 'group-id-1',
|
searchConversation: getDefaultConversation(),
|
||||||
searchConversationName: 'Everyone',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return <MainHeader {...props} />;
|
return <MainHeader {...props} />;
|
||||||
|
@ -112,8 +92,7 @@ story.add('Searching Conversation with Term', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
name: 'John Smith',
|
name: 'John Smith',
|
||||||
searchTerm: 'address',
|
searchTerm: 'address',
|
||||||
searchConversationId: 'group-id-1',
|
searchConversation: getDefaultConversation(),
|
||||||
searchConversationName: 'Everyone',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return <MainHeader {...props} />;
|
return <MainHeader {...props} />;
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { debounce, get } from 'lodash';
|
|
||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
@ -13,20 +11,14 @@ import { AvatarPopup } from './AvatarPopup';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import type { AvatarColorType } from '../types/Colors';
|
import type { AvatarColorType } from '../types/Colors';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { LeftPaneSearchInput } from './LeftPaneSearchInput';
|
||||||
|
|
||||||
export type PropsType = {
|
export type PropsType = {
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
searchConversationName?: string;
|
searchConversation: undefined | ConversationType;
|
||||||
searchConversationId?: string;
|
|
||||||
startSearchCounter: number;
|
startSearchCounter: number;
|
||||||
selectedConversation: undefined | ConversationType;
|
selectedConversation: undefined | ConversationType;
|
||||||
|
|
||||||
// To be used as an ID
|
|
||||||
ourConversationId: string;
|
|
||||||
ourUuid: string;
|
|
||||||
ourNumber: string;
|
|
||||||
regionCode: string;
|
|
||||||
|
|
||||||
// For display
|
// For display
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
isMe?: boolean;
|
isMe?: boolean;
|
||||||
|
@ -42,24 +34,6 @@ export type PropsType = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
|
||||||
updateSearchTerm: (searchTerm: string) => void;
|
updateSearchTerm: (searchTerm: string) => void;
|
||||||
startSearch: () => void;
|
|
||||||
searchInConversation: (id: string, name: string) => void;
|
|
||||||
searchMessages: (
|
|
||||||
query: string,
|
|
||||||
options: {
|
|
||||||
searchConversationId?: string;
|
|
||||||
regionCode: string;
|
|
||||||
}
|
|
||||||
) => void;
|
|
||||||
searchDiscussions: (
|
|
||||||
query: string,
|
|
||||||
options: {
|
|
||||||
ourConversationId: string;
|
|
||||||
ourNumber: string;
|
|
||||||
ourUuid: string;
|
|
||||||
noteToSelf: string;
|
|
||||||
}
|
|
||||||
) => void;
|
|
||||||
startUpdate: () => unknown;
|
startUpdate: () => unknown;
|
||||||
clearConversationSearch: () => void;
|
clearConversationSearch: () => void;
|
||||||
clearSearch: () => void;
|
clearSearch: () => void;
|
||||||
|
@ -89,12 +63,12 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: PropsType): void {
|
public componentDidUpdate(prevProps: PropsType): void {
|
||||||
const { searchConversationId, startSearchCounter } = this.props;
|
const { searchConversation, startSearchCounter } = this.props;
|
||||||
|
|
||||||
// When user chooses to search in a given conversation we focus the field for them
|
// When user chooses to search in a given conversation we focus the field for them
|
||||||
if (
|
if (
|
||||||
searchConversationId &&
|
searchConversation &&
|
||||||
searchConversationId !== prevProps.searchConversationId
|
searchConversation.id !== prevProps.searchConversation?.id
|
||||||
) {
|
) {
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
}
|
}
|
||||||
|
@ -157,46 +131,16 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public search = debounce((searchTerm: string): void => {
|
private updateSearch = (searchTerm: string): void => {
|
||||||
const {
|
|
||||||
i18n,
|
|
||||||
ourConversationId,
|
|
||||||
ourNumber,
|
|
||||||
ourUuid,
|
|
||||||
regionCode,
|
|
||||||
searchDiscussions,
|
|
||||||
searchMessages,
|
|
||||||
searchConversationId,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (searchDiscussions && !searchConversationId) {
|
|
||||||
searchDiscussions(searchTerm, {
|
|
||||||
noteToSelf: i18n('noteToSelf').toLowerCase(),
|
|
||||||
ourConversationId,
|
|
||||||
ourNumber,
|
|
||||||
ourUuid,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchMessages) {
|
|
||||||
searchMessages(searchTerm, {
|
|
||||||
searchConversationId,
|
|
||||||
regionCode,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
public updateSearch = (event: React.FormEvent<HTMLInputElement>): void => {
|
|
||||||
const {
|
const {
|
||||||
updateSearchTerm,
|
updateSearchTerm,
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearSearch,
|
clearSearch,
|
||||||
searchConversationId,
|
searchConversation,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const searchTerm = event.currentTarget.value;
|
|
||||||
|
|
||||||
if (!searchTerm) {
|
if (!searchTerm) {
|
||||||
if (searchConversationId) {
|
if (searchConversation) {
|
||||||
clearConversationSearch();
|
clearConversationSearch();
|
||||||
} else {
|
} else {
|
||||||
clearSearch();
|
clearSearch();
|
||||||
|
@ -208,132 +152,30 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
||||||
if (updateSearchTerm) {
|
if (updateSearchTerm) {
|
||||||
updateSearchTerm(searchTerm);
|
updateSearchTerm(searchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchTerm.length < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.search(searchTerm);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public clearSearch = (): void => {
|
public clearSearch = (): void => {
|
||||||
const { clearSearch } = this.props;
|
const { clearSearch } = this.props;
|
||||||
|
|
||||||
clearSearch();
|
clearSearch();
|
||||||
this.setFocus();
|
this.setFocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
public clearConversationSearch = (): void => {
|
|
||||||
const { clearConversationSearch } = this.props;
|
|
||||||
|
|
||||||
clearConversationSearch();
|
|
||||||
this.setFocus();
|
|
||||||
};
|
|
||||||
|
|
||||||
private handleInputBlur = (): void => {
|
private handleInputBlur = (): void => {
|
||||||
const { clearSearch, searchConversationId, searchTerm } = this.props;
|
const { clearSearch, searchConversation, searchTerm } = this.props;
|
||||||
if (!searchConversationId && !searchTerm) {
|
if (!searchConversation && !searchTerm) {
|
||||||
clearSearch();
|
clearSearch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleInputKeyDown = (
|
|
||||||
event: React.KeyboardEvent<HTMLInputElement>
|
|
||||||
): void => {
|
|
||||||
const {
|
|
||||||
clearConversationSearch,
|
|
||||||
clearSearch,
|
|
||||||
searchConversationId,
|
|
||||||
searchTerm,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { ctrlKey, metaKey, key } = event;
|
|
||||||
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
|
|
||||||
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
|
|
||||||
const commandOrCtrl = commandKey || controlKey;
|
|
||||||
|
|
||||||
// On linux, this keyboard combination selects all text
|
|
||||||
if (commandOrCtrl && key === '/') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key !== 'Escape') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchConversationId && searchTerm) {
|
|
||||||
clearConversationSearch();
|
|
||||||
} else {
|
|
||||||
clearSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
public handleGlobalKeyDown = (event: KeyboardEvent): void => {
|
public handleGlobalKeyDown = (event: KeyboardEvent): void => {
|
||||||
const { showingAvatarPopup } = this.state;
|
const { showingAvatarPopup } = this.state;
|
||||||
const {
|
const { key } = event;
|
||||||
i18n,
|
|
||||||
selectedConversation,
|
|
||||||
startSearch,
|
|
||||||
searchInConversation,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { ctrlKey, metaKey, shiftKey, key } = event;
|
|
||||||
const commandKey = get(window, 'platform') === 'darwin' && metaKey;
|
|
||||||
const controlKey = get(window, 'platform') !== 'darwin' && ctrlKey;
|
|
||||||
const commandOrCtrl = commandKey || controlKey;
|
|
||||||
const commandAndCtrl = commandKey && ctrlKey;
|
|
||||||
|
|
||||||
if (showingAvatarPopup && key === 'Escape') {
|
if (showingAvatarPopup && key === 'Escape') {
|
||||||
this.hideAvatarPopup();
|
this.hideAvatarPopup();
|
||||||
} else if (
|
|
||||||
commandOrCtrl &&
|
|
||||||
!commandAndCtrl &&
|
|
||||||
!shiftKey &&
|
|
||||||
(key === 'f' || key === 'F')
|
|
||||||
) {
|
|
||||||
startSearch();
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
} else if (
|
|
||||||
selectedConversation &&
|
|
||||||
commandOrCtrl &&
|
|
||||||
!commandAndCtrl &&
|
|
||||||
shiftKey &&
|
|
||||||
(key === 'f' || key === 'F')
|
|
||||||
) {
|
|
||||||
const name = selectedConversation.isMe
|
|
||||||
? i18n('noteToSelf')
|
|
||||||
: selectedConversation.title;
|
|
||||||
searchInConversation(selectedConversation.id, name);
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public handleXButton = (): void => {
|
|
||||||
const {
|
|
||||||
searchConversationId,
|
|
||||||
clearConversationSearch,
|
|
||||||
clearSearch,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (searchConversationId) {
|
|
||||||
clearConversationSearch();
|
|
||||||
} else {
|
|
||||||
clearSearch();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setFocus();
|
|
||||||
};
|
|
||||||
|
|
||||||
public setFocus = (): void => {
|
public setFocus = (): void => {
|
||||||
if (this.inputRef.current) {
|
if (this.inputRef.current) {
|
||||||
this.inputRef.current.focus();
|
this.inputRef.current.focus();
|
||||||
|
@ -356,8 +198,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
||||||
name,
|
name,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
profileName,
|
profileName,
|
||||||
searchConversationId,
|
searchConversation,
|
||||||
searchConversationName,
|
|
||||||
searchTerm,
|
searchTerm,
|
||||||
showArchivedConversations,
|
showArchivedConversations,
|
||||||
startComposing,
|
startComposing,
|
||||||
|
@ -367,13 +208,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { showingAvatarPopup, popperRoot } = this.state;
|
const { showingAvatarPopup, popperRoot } = this.state;
|
||||||
|
|
||||||
const placeholder = searchConversationName
|
const isSearching = Boolean(searchConversation || searchTerm.trim().length);
|
||||||
? i18n('searchIn', [searchConversationName])
|
|
||||||
: i18n('search');
|
|
||||||
|
|
||||||
const isSearching = Boolean(
|
|
||||||
searchConversationId || searchTerm.trim().length
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-main-header">
|
<div className="module-main-header">
|
||||||
|
@ -447,59 +282,16 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
</Manager>
|
</Manager>
|
||||||
<div className="module-main-header__search">
|
<LeftPaneSearchInput
|
||||||
{searchConversationId ? (
|
disabled={disabled}
|
||||||
<button
|
i18n={i18n}
|
||||||
className="module-main-header__search__in-conversation-pill"
|
onBlur={this.handleInputBlur}
|
||||||
onClick={this.clearSearch}
|
onChangeValue={this.updateSearch}
|
||||||
tabIndex={-1}
|
onClear={this.clearSearch}
|
||||||
type="button"
|
ref={this.inputRef}
|
||||||
aria-label={i18n('clearSearch')}
|
searchConversation={searchConversation}
|
||||||
>
|
value={searchTerm}
|
||||||
<div className="module-main-header__search__in-conversation-pill__avatar-container">
|
/>
|
||||||
<div className="module-main-header__search__in-conversation-pill__avatar" />
|
|
||||||
</div>
|
|
||||||
<div className="module-main-header__search__in-conversation-pill__x-button" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="module-main-header__search__icon"
|
|
||||||
onClick={this.setFocus}
|
|
||||||
tabIndex={-1}
|
|
||||||
type="button"
|
|
||||||
aria-label={i18n('search')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
disabled={disabled}
|
|
||||||
type="text"
|
|
||||||
ref={this.inputRef}
|
|
||||||
className={classNames(
|
|
||||||
'module-main-header__search__input',
|
|
||||||
searchTerm
|
|
||||||
? 'module-main-header__search__input--with-text'
|
|
||||||
: null,
|
|
||||||
searchConversationId
|
|
||||||
? 'module-main-header__search__input--in-conversation'
|
|
||||||
: null
|
|
||||||
)}
|
|
||||||
placeholder={placeholder}
|
|
||||||
dir="auto"
|
|
||||||
onBlur={this.handleInputBlur}
|
|
||||||
onKeyDown={this.handleInputKeyDown}
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={this.updateSearch}
|
|
||||||
/>
|
|
||||||
{searchTerm ? (
|
|
||||||
<button
|
|
||||||
tabIndex={-1}
|
|
||||||
className="module-main-header__search__cancel-icon"
|
|
||||||
onClick={this.handleXButton}
|
|
||||||
type="button"
|
|
||||||
aria-label={i18n('cancel')}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{!isSearching && (
|
{!isSearching && (
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('newConversation')}
|
aria-label={i18n('newConversation')}
|
||||||
|
|
|
@ -97,7 +97,7 @@ type ActionProps = {
|
||||||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||||
showContactModal: (contactId: string, conversationId: string) => void;
|
showContactModal: (contactId: string, conversationId: string) => void;
|
||||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||||
searchInConversation: (id: string, title: string) => unknown;
|
searchInConversation: (id: string) => unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = StateProps & ActionProps;
|
export type Props = StateProps & ActionProps;
|
||||||
|
@ -365,10 +365,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
<Button
|
<Button
|
||||||
icon={ButtonIconType.search}
|
icon={ButtonIconType.search}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
searchInConversation(
|
searchInConversation(conversation.id);
|
||||||
conversation.id,
|
|
||||||
conversation.isMe ? i18n('noteToSelf') : conversation.title
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
variant={ButtonVariant.Details}
|
variant={ButtonVariant.Details}
|
||||||
>
|
>
|
||||||
|
|
|
@ -12,28 +12,54 @@ import type { Row } from '../ConversationList';
|
||||||
import { RowType } from '../ConversationList';
|
import { RowType } from '../ConversationList';
|
||||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
import { LeftPaneSearchInput } from '../LeftPaneSearchInput';
|
||||||
|
import type { LeftPaneSearchPropsType } from './LeftPaneSearchHelper';
|
||||||
|
import { LeftPaneSearchHelper } from './LeftPaneSearchHelper';
|
||||||
|
|
||||||
export type LeftPaneArchivePropsType = {
|
type LeftPaneArchiveBasePropsType = {
|
||||||
archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||||
|
searchConversation: undefined | ConversationType;
|
||||||
|
searchTerm: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LeftPaneArchivePropsType =
|
||||||
|
| LeftPaneArchiveBasePropsType
|
||||||
|
| (LeftPaneArchiveBasePropsType & LeftPaneSearchPropsType);
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
/* eslint-disable class-methods-use-this */
|
||||||
|
|
||||||
export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsType> {
|
export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsType> {
|
||||||
private readonly archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
private readonly archivedConversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||||
|
|
||||||
constructor({ archivedConversations }: Readonly<LeftPaneArchivePropsType>) {
|
private readonly searchConversation: undefined | ConversationType;
|
||||||
|
|
||||||
|
private readonly searchTerm: string;
|
||||||
|
|
||||||
|
private readonly searchHelper: undefined | LeftPaneSearchHelper;
|
||||||
|
|
||||||
|
constructor(props: Readonly<LeftPaneArchivePropsType>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.archivedConversations = archivedConversations;
|
this.archivedConversations = props.archivedConversations;
|
||||||
|
this.searchConversation = props.searchConversation;
|
||||||
|
this.searchTerm = props.searchTerm;
|
||||||
|
|
||||||
|
if ('conversationResults' in props) {
|
||||||
|
this.searchHelper = new LeftPaneSearchHelper(props);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeaderContents({
|
getHeaderContents({
|
||||||
|
clearSearch,
|
||||||
i18n,
|
i18n,
|
||||||
showInbox,
|
showInbox,
|
||||||
|
updateSearchTerm,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
|
clearSearch: () => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
showInbox: () => void;
|
showInbox: () => void;
|
||||||
|
updateSearchTerm: (query: string) => void;
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
return (
|
return (
|
||||||
<div className="module-left-pane__header__contents">
|
<div className="module-left-pane__header__contents">
|
||||||
|
@ -45,7 +71,24 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
/>
|
||||||
<div className="module-left-pane__header__contents__text">
|
<div className="module-left-pane__header__contents__text">
|
||||||
{i18n('archivedConversations')}
|
{this.searchConversation ? (
|
||||||
|
<LeftPaneSearchInput
|
||||||
|
i18n={i18n}
|
||||||
|
onChangeValue={newValue => {
|
||||||
|
updateSearchTerm(newValue);
|
||||||
|
}}
|
||||||
|
onClear={() => {
|
||||||
|
clearSearch();
|
||||||
|
}}
|
||||||
|
ref={el => {
|
||||||
|
el?.focus();
|
||||||
|
}}
|
||||||
|
searchConversation={this.searchConversation}
|
||||||
|
value={this.searchTerm}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
i18n('archivedConversations')
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -55,7 +98,13 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
return showInbox;
|
return showInbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreRowsNode({ i18n }: Readonly<{ i18n: LocalizerType }>): ReactChild {
|
getPreRowsNode({
|
||||||
|
i18n,
|
||||||
|
}: Readonly<{ i18n: LocalizerType }>): ReactChild | null {
|
||||||
|
if (this.searchHelper) {
|
||||||
|
return this.searchHelper.getPreRowsNode({ i18n });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-left-pane__archive-helper-text">
|
<div className="module-left-pane__archive-helper-text">
|
||||||
{i18n('archiveHelperText')}
|
{i18n('archiveHelperText')}
|
||||||
|
@ -64,10 +113,16 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowCount(): number {
|
getRowCount(): number {
|
||||||
return this.archivedConversations.length;
|
return (
|
||||||
|
this.searchHelper?.getRowCount() ?? this.archivedConversations.length
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRow(rowIndex: number): undefined | Row {
|
getRow(rowIndex: number): undefined | Row {
|
||||||
|
if (this.searchHelper) {
|
||||||
|
return this.searchHelper.getRow(rowIndex);
|
||||||
|
}
|
||||||
|
|
||||||
const conversation = this.archivedConversations[rowIndex];
|
const conversation = this.archivedConversations[rowIndex];
|
||||||
return conversation
|
return conversation
|
||||||
? {
|
? {
|
||||||
|
@ -80,6 +135,10 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
getRowIndexToScrollTo(
|
getRowIndexToScrollTo(
|
||||||
selectedConversationId: undefined | string
|
selectedConversationId: undefined | string
|
||||||
): undefined | number {
|
): undefined | number {
|
||||||
|
if (this.searchHelper) {
|
||||||
|
return this.searchHelper.getRowIndexToScrollTo(selectedConversationId);
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectedConversationId) {
|
if (!selectedConversationId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -92,7 +151,12 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
getConversationAndMessageAtIndex(
|
getConversationAndMessageAtIndex(
|
||||||
conversationIndex: number
|
conversationIndex: number
|
||||||
): undefined | { conversationId: string } {
|
): undefined | { conversationId: string } {
|
||||||
const { archivedConversations } = this;
|
const { archivedConversations, searchHelper } = this;
|
||||||
|
|
||||||
|
if (searchHelper) {
|
||||||
|
return searchHelper.getConversationAndMessageAtIndex(conversationIndex);
|
||||||
|
}
|
||||||
|
|
||||||
const conversation =
|
const conversation =
|
||||||
archivedConversations[conversationIndex] || last(archivedConversations);
|
archivedConversations[conversationIndex] || last(archivedConversations);
|
||||||
return conversation ? { conversationId: conversation.id } : undefined;
|
return conversation ? { conversationId: conversation.id } : undefined;
|
||||||
|
@ -101,8 +165,16 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
getConversationAndMessageInDirection(
|
getConversationAndMessageInDirection(
|
||||||
toFind: Readonly<ToFindType>,
|
toFind: Readonly<ToFindType>,
|
||||||
selectedConversationId: undefined | string,
|
selectedConversationId: undefined | string,
|
||||||
_selectedMessageId: unknown
|
selectedMessageId: unknown
|
||||||
): undefined | { conversationId: string } {
|
): undefined | { conversationId: string } {
|
||||||
|
if (this.searchHelper) {
|
||||||
|
return this.searchHelper.getConversationAndMessageInDirection(
|
||||||
|
toFind,
|
||||||
|
selectedConversationId,
|
||||||
|
selectedMessageId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return getConversationInDirection(
|
return getConversationInDirection(
|
||||||
this.archivedConversations,
|
this.archivedConversations,
|
||||||
toFind,
|
toFind,
|
||||||
|
@ -110,7 +182,51 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRecomputeRowHeights(_old: unknown): boolean {
|
shouldRecomputeRowHeights(old: Readonly<LeftPaneArchivePropsType>): boolean {
|
||||||
|
const hasSearchingChanged =
|
||||||
|
'conversationResults' in old !== Boolean(this.searchHelper);
|
||||||
|
if (hasSearchingChanged) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('conversationResults' in old && this.searchHelper) {
|
||||||
|
return this.searchHelper.shouldRecomputeRowHeights(old);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown(
|
||||||
|
event: KeyboardEvent,
|
||||||
|
{
|
||||||
|
searchInConversation,
|
||||||
|
selectedConversationId,
|
||||||
|
}: Readonly<{
|
||||||
|
searchInConversation: (conversationId: string) => unknown;
|
||||||
|
selectedConversationId: undefined | string;
|
||||||
|
}>
|
||||||
|
): void {
|
||||||
|
if (!selectedConversationId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ctrlKey, metaKey, shiftKey, key } = event;
|
||||||
|
const commandKey = window.platform === 'darwin' && metaKey;
|
||||||
|
const controlKey = window.platform !== 'darwin' && ctrlKey;
|
||||||
|
const commandOrCtrl = commandKey || controlKey;
|
||||||
|
const commandAndCtrl = commandKey && ctrlKey;
|
||||||
|
|
||||||
|
if (
|
||||||
|
commandOrCtrl &&
|
||||||
|
!commandAndCtrl &&
|
||||||
|
shiftKey &&
|
||||||
|
key.toLowerCase() === 'f' &&
|
||||||
|
this.archivedConversations.some(({ id }) => id === selectedConversationId)
|
||||||
|
) {
|
||||||
|
searchInConversation(selectedConversationId);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,12 @@ export type ToFindType = {
|
||||||
export abstract class LeftPaneHelper<T> {
|
export abstract class LeftPaneHelper<T> {
|
||||||
getHeaderContents(
|
getHeaderContents(
|
||||||
_: Readonly<{
|
_: Readonly<{
|
||||||
|
clearSearch: () => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
showInbox: () => void;
|
showInbox: () => void;
|
||||||
startComposing: () => void;
|
startComposing: () => void;
|
||||||
showChooseGroupMembers: () => void;
|
showChooseGroupMembers: () => void;
|
||||||
|
updateSearchTerm: (query: string) => void;
|
||||||
}>
|
}>
|
||||||
): null | ReactChild {
|
): null | ReactChild {
|
||||||
return null;
|
return null;
|
||||||
|
@ -97,6 +99,17 @@ export abstract class LeftPaneHelper<T> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown(
|
||||||
|
_event: KeyboardEvent,
|
||||||
|
_options: Readonly<{
|
||||||
|
searchInConversation: (conversationId: string) => unknown;
|
||||||
|
selectedConversationId: undefined | string;
|
||||||
|
startSearch: () => unknown;
|
||||||
|
}>
|
||||||
|
): void {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
abstract getConversationAndMessageAtIndex(
|
abstract getConversationAndMessageAtIndex(
|
||||||
conversationIndex: number
|
conversationIndex: number
|
||||||
): undefined | { conversationId: string; messageId?: string };
|
): undefined | { conversationId: string; messageId?: string };
|
||||||
|
|
|
@ -13,6 +13,7 @@ import type { Row } from '../ConversationList';
|
||||||
import { RowType } from '../ConversationList';
|
import { RowType } from '../ConversationList';
|
||||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||||
import type { LocalizerType } from '../../types/Util';
|
import type { LocalizerType } from '../../types/Util';
|
||||||
|
import { handleKeydownForSearch } from './handleKeydownForSearch';
|
||||||
|
|
||||||
export type LeftPaneInboxPropsType = {
|
export type LeftPaneInboxPropsType = {
|
||||||
conversations: ReadonlyArray<ConversationListItemPropsType>;
|
conversations: ReadonlyArray<ConversationListItemPropsType>;
|
||||||
|
@ -229,6 +230,17 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown(
|
||||||
|
event: KeyboardEvent,
|
||||||
|
options: Readonly<{
|
||||||
|
searchInConversation: (conversationId: string) => unknown;
|
||||||
|
selectedConversationId: undefined | string;
|
||||||
|
startSearch: () => unknown;
|
||||||
|
}>
|
||||||
|
): void {
|
||||||
|
handleKeydownForSearch(event, options);
|
||||||
|
}
|
||||||
|
|
||||||
private hasPinnedAndNonpinned(): boolean {
|
private hasPinnedAndNonpinned(): boolean {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
this.pinnedConversations.length && this.conversations.length
|
this.pinnedConversations.length && this.conversations.length
|
||||||
|
|
|
@ -10,6 +10,7 @@ import type { LocalizerType } from '../../types/Util';
|
||||||
import type { Row } from '../ConversationList';
|
import type { Row } from '../ConversationList';
|
||||||
import { RowType } from '../ConversationList';
|
import { RowType } from '../ConversationList';
|
||||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||||
|
import { handleKeydownForSearch } from './handleKeydownForSearch';
|
||||||
|
|
||||||
import { Intl } from '../Intl';
|
import { Intl } from '../Intl';
|
||||||
import { Emojify } from '../conversation/Emojify';
|
import { Emojify } from '../conversation/Emojify';
|
||||||
|
@ -42,6 +43,8 @@ const searchResultKeys: Array<
|
||||||
'conversationResults' | 'contactResults' | 'messageResults'
|
'conversationResults' | 'contactResults' | 'messageResults'
|
||||||
> = ['conversationResults', 'contactResults', 'messageResults'];
|
> = ['conversationResults', 'contactResults', 'messageResults'];
|
||||||
|
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
|
||||||
export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType> {
|
export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType> {
|
||||||
private readonly conversationResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;
|
private readonly conversationResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;
|
||||||
|
|
||||||
|
@ -270,6 +273,17 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onKeyDown(
|
||||||
|
event: KeyboardEvent,
|
||||||
|
options: Readonly<{
|
||||||
|
searchInConversation: (conversationId: string) => unknown;
|
||||||
|
selectedConversationId: undefined | string;
|
||||||
|
startSearch: () => unknown;
|
||||||
|
}>
|
||||||
|
): void {
|
||||||
|
handleKeydownForSearch(event, options);
|
||||||
|
}
|
||||||
|
|
||||||
private allResults() {
|
private allResults() {
|
||||||
return [this.conversationResults, this.contactResults, this.messageResults];
|
return [this.conversationResults, this.contactResults, this.messageResults];
|
||||||
}
|
}
|
||||||
|
|
33
ts/components/leftPane/handleKeydownForSearch.ts
Normal file
33
ts/components/leftPane/handleKeydownForSearch.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
export function handleKeydownForSearch(
|
||||||
|
event: Readonly<KeyboardEvent>,
|
||||||
|
{
|
||||||
|
searchInConversation,
|
||||||
|
selectedConversationId,
|
||||||
|
startSearch,
|
||||||
|
}: Readonly<{
|
||||||
|
searchInConversation: (conversationId: string) => unknown;
|
||||||
|
selectedConversationId: undefined | string;
|
||||||
|
startSearch: () => unknown;
|
||||||
|
}>
|
||||||
|
): void {
|
||||||
|
const { ctrlKey, metaKey, shiftKey, key } = event;
|
||||||
|
const commandKey = window.platform === 'darwin' && metaKey;
|
||||||
|
const controlKey = window.platform !== 'darwin' && ctrlKey;
|
||||||
|
const commandOrCtrl = commandKey || controlKey;
|
||||||
|
const commandAndCtrl = commandKey && ctrlKey;
|
||||||
|
|
||||||
|
if (commandOrCtrl && !commandAndCtrl && key.toLowerCase() === 'f') {
|
||||||
|
if (!shiftKey) {
|
||||||
|
startSearch();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else if (selectedConversationId) {
|
||||||
|
searchInConversation(selectedConversationId);
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { omit, reject } from 'lodash';
|
import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { debounce, omit, reject } from 'lodash';
|
||||||
|
|
||||||
import { normalize } from '../../types/PhoneNumber';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
|
||||||
import type {
|
import type {
|
||||||
ClientSearchResultMessageType,
|
ClientSearchResultMessageType,
|
||||||
|
@ -21,6 +22,8 @@ import type {
|
||||||
SelectedConversationChangedActionType,
|
SelectedConversationChangedActionType,
|
||||||
ShowArchivedConversationsActionType,
|
ShowArchivedConversationsActionType,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
|
import { getQuery, getSearchConversation } from '../selectors/search';
|
||||||
|
import { getIntl, getUserConversationId } from '../selectors/user';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
searchConversations: dataSearchConversations,
|
searchConversations: dataSearchConversations,
|
||||||
|
@ -41,11 +44,9 @@ export type MessageSearchResultLookupType = {
|
||||||
export type SearchStateType = {
|
export type SearchStateType = {
|
||||||
startSearchCounter: number;
|
startSearchCounter: number;
|
||||||
searchConversationId?: string;
|
searchConversationId?: string;
|
||||||
searchConversationName?: string;
|
|
||||||
contactIds: Array<string>;
|
contactIds: Array<string>;
|
||||||
conversationIds: Array<string>;
|
conversationIds: Array<string>;
|
||||||
query: string;
|
query: string;
|
||||||
normalizedPhoneNumber?: string;
|
|
||||||
messageIds: Array<string>;
|
messageIds: Array<string>;
|
||||||
// We do store message data to pass through the selector
|
// We do store message data to pass through the selector
|
||||||
messageLookup: MessageSearchResultLookupType;
|
messageLookup: MessageSearchResultLookupType;
|
||||||
|
@ -57,33 +58,20 @@ export type SearchStateType = {
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
type SearchResultsBaseType = {
|
|
||||||
query: string;
|
|
||||||
normalizedPhoneNumber?: string;
|
|
||||||
};
|
|
||||||
type SearchMessagesResultsPayloadType = SearchResultsBaseType & {
|
|
||||||
messages: Array<MessageSearchResultType>;
|
|
||||||
};
|
|
||||||
type SearchDiscussionsResultsPayloadType = SearchResultsBaseType & {
|
|
||||||
conversationIds: Array<string>;
|
|
||||||
contactIds: Array<string>;
|
|
||||||
};
|
|
||||||
type SearchMessagesResultsKickoffActionType = {
|
|
||||||
type: 'SEARCH_MESSAGES_RESULTS';
|
|
||||||
payload: Promise<SearchMessagesResultsPayloadType>;
|
|
||||||
};
|
|
||||||
type SearchDiscussionsResultsKickoffActionType = {
|
|
||||||
type: 'SEARCH_DISCUSSIONS_RESULTS';
|
|
||||||
payload: Promise<SearchDiscussionsResultsPayloadType>;
|
|
||||||
};
|
|
||||||
|
|
||||||
type SearchMessagesResultsFulfilledActionType = {
|
type SearchMessagesResultsFulfilledActionType = {
|
||||||
type: 'SEARCH_MESSAGES_RESULTS_FULFILLED';
|
type: 'SEARCH_MESSAGES_RESULTS_FULFILLED';
|
||||||
payload: SearchMessagesResultsPayloadType;
|
payload: {
|
||||||
|
messages: Array<MessageSearchResultType>;
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
type SearchDiscussionsResultsFulfilledActionType = {
|
type SearchDiscussionsResultsFulfilledActionType = {
|
||||||
type: 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED';
|
type: 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED';
|
||||||
payload: SearchDiscussionsResultsPayloadType;
|
payload: {
|
||||||
|
conversationIds: Array<string>;
|
||||||
|
contactIds: Array<string>;
|
||||||
|
query: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
type UpdateSearchTermActionType = {
|
type UpdateSearchTermActionType = {
|
||||||
type: 'SEARCH_UPDATE';
|
type: 'SEARCH_UPDATE';
|
||||||
|
@ -105,15 +93,10 @@ type ClearConversationSearchActionType = {
|
||||||
};
|
};
|
||||||
type SearchInConversationActionType = {
|
type SearchInConversationActionType = {
|
||||||
type: 'SEARCH_IN_CONVERSATION';
|
type: 'SEARCH_IN_CONVERSATION';
|
||||||
payload: {
|
payload: { searchConversationId: string };
|
||||||
searchConversationId: string;
|
|
||||||
searchConversationName: string;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SearchActionType =
|
export type SearchActionType =
|
||||||
| SearchMessagesResultsKickoffActionType
|
|
||||||
| SearchDiscussionsResultsKickoffActionType
|
|
||||||
| SearchMessagesResultsFulfilledActionType
|
| SearchMessagesResultsFulfilledActionType
|
||||||
| SearchDiscussionsResultsFulfilledActionType
|
| SearchDiscussionsResultsFulfilledActionType
|
||||||
| UpdateSearchTermActionType
|
| UpdateSearchTermActionType
|
||||||
|
@ -130,8 +113,6 @@ export type SearchActionType =
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
searchMessages,
|
|
||||||
searchDiscussions,
|
|
||||||
startSearch,
|
startSearch,
|
||||||
clearSearch,
|
clearSearch,
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
|
@ -139,72 +120,6 @@ export const actions = {
|
||||||
updateSearchTerm,
|
updateSearchTerm,
|
||||||
};
|
};
|
||||||
|
|
||||||
function searchMessages(
|
|
||||||
query: string,
|
|
||||||
options: {
|
|
||||||
regionCode: string;
|
|
||||||
}
|
|
||||||
): SearchMessagesResultsKickoffActionType {
|
|
||||||
return {
|
|
||||||
type: 'SEARCH_MESSAGES_RESULTS',
|
|
||||||
payload: doSearchMessages(query, options),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchDiscussions(
|
|
||||||
query: string,
|
|
||||||
options: {
|
|
||||||
ourConversationId: string;
|
|
||||||
noteToSelf: string;
|
|
||||||
}
|
|
||||||
): SearchDiscussionsResultsKickoffActionType {
|
|
||||||
return {
|
|
||||||
type: 'SEARCH_DISCUSSIONS_RESULTS',
|
|
||||||
payload: doSearchDiscussions(query, options),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doSearchMessages(
|
|
||||||
query: string,
|
|
||||||
options: {
|
|
||||||
searchConversationId?: string;
|
|
||||||
regionCode: string;
|
|
||||||
}
|
|
||||||
): Promise<SearchMessagesResultsPayloadType> {
|
|
||||||
const { regionCode, searchConversationId } = options;
|
|
||||||
const normalizedPhoneNumber = normalize(query, { regionCode });
|
|
||||||
|
|
||||||
const messages = await queryMessages(query, searchConversationId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
messages,
|
|
||||||
normalizedPhoneNumber,
|
|
||||||
query,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doSearchDiscussions(
|
|
||||||
query: string,
|
|
||||||
options: {
|
|
||||||
ourConversationId: string;
|
|
||||||
noteToSelf: string;
|
|
||||||
}
|
|
||||||
): Promise<SearchDiscussionsResultsPayloadType> {
|
|
||||||
const { ourConversationId, noteToSelf } = options;
|
|
||||||
const { conversationIds, contactIds } = await queryConversationsAndContacts(
|
|
||||||
query,
|
|
||||||
{
|
|
||||||
ourConversationId,
|
|
||||||
noteToSelf,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
conversationIds,
|
|
||||||
contactIds,
|
|
||||||
query,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function startSearch(): StartSearchActionType {
|
function startSearch(): StartSearchActionType {
|
||||||
return {
|
return {
|
||||||
type: 'SEARCH_START',
|
type: 'SEARCH_START',
|
||||||
|
@ -224,27 +139,92 @@ function clearConversationSearch(): ClearConversationSearchActionType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function searchInConversation(
|
function searchInConversation(
|
||||||
searchConversationId: string,
|
searchConversationId: string
|
||||||
searchConversationName: string
|
|
||||||
): SearchInConversationActionType {
|
): SearchInConversationActionType {
|
||||||
return {
|
return {
|
||||||
type: 'SEARCH_IN_CONVERSATION',
|
type: 'SEARCH_IN_CONVERSATION',
|
||||||
payload: {
|
payload: { searchConversationId },
|
||||||
searchConversationId,
|
|
||||||
searchConversationName,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSearchTerm(query: string): UpdateSearchTermActionType {
|
function updateSearchTerm(
|
||||||
return {
|
query: string
|
||||||
type: 'SEARCH_UPDATE',
|
): ThunkAction<void, RootStateType, unknown, UpdateSearchTermActionType> {
|
||||||
payload: {
|
return (dispatch, getState) => {
|
||||||
query,
|
dispatch({
|
||||||
},
|
type: 'SEARCH_UPDATE',
|
||||||
|
payload: { query },
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
doSearch({
|
||||||
|
dispatch,
|
||||||
|
noteToSelf: getIntl(state)('noteToSelf').toLowerCase(),
|
||||||
|
ourConversationId: getUserConversationId(state),
|
||||||
|
query: getQuery(state),
|
||||||
|
searchConversationId: getSearchConversation(state)?.id,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const doSearch = debounce(
|
||||||
|
({
|
||||||
|
dispatch,
|
||||||
|
noteToSelf,
|
||||||
|
ourConversationId,
|
||||||
|
query,
|
||||||
|
searchConversationId,
|
||||||
|
}: Readonly<{
|
||||||
|
dispatch: ThunkDispatch<
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
| SearchMessagesResultsFulfilledActionType
|
||||||
|
| SearchDiscussionsResultsFulfilledActionType
|
||||||
|
>;
|
||||||
|
noteToSelf: string;
|
||||||
|
ourConversationId: string;
|
||||||
|
query: string;
|
||||||
|
searchConversationId: undefined | string;
|
||||||
|
}>) => {
|
||||||
|
if (!query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SEARCH_MESSAGES_RESULTS_FULFILLED',
|
||||||
|
payload: {
|
||||||
|
messages: await queryMessages(query, searchConversationId),
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!searchConversationId) {
|
||||||
|
(async () => {
|
||||||
|
const {
|
||||||
|
conversationIds,
|
||||||
|
contactIds,
|
||||||
|
} = await queryConversationsAndContacts(query, {
|
||||||
|
ourConversationId,
|
||||||
|
noteToSelf,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED',
|
||||||
|
payload: {
|
||||||
|
conversationIds,
|
||||||
|
contactIds,
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
200
|
||||||
|
);
|
||||||
|
|
||||||
async function queryMessages(
|
async function queryMessages(
|
||||||
query: string,
|
query: string,
|
||||||
searchConversationId?: string
|
searchConversationId?: string
|
||||||
|
@ -342,7 +322,6 @@ export function reducer(
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
searchConversationId: undefined,
|
searchConversationId: undefined,
|
||||||
searchConversationName: undefined,
|
|
||||||
startSearchCounter: state.startSearchCounter + 1,
|
startSearchCounter: state.startSearchCounter + 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -376,7 +355,7 @@ export function reducer(
|
||||||
|
|
||||||
if (action.type === 'SEARCH_IN_CONVERSATION') {
|
if (action.type === 'SEARCH_IN_CONVERSATION') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { searchConversationId, searchConversationName } = payload;
|
const { searchConversationId } = payload;
|
||||||
|
|
||||||
if (searchConversationId === state.searchConversationId) {
|
if (searchConversationId === state.searchConversationId) {
|
||||||
return {
|
return {
|
||||||
|
@ -388,23 +367,21 @@ export function reducer(
|
||||||
return {
|
return {
|
||||||
...getEmptyState(),
|
...getEmptyState(),
|
||||||
searchConversationId,
|
searchConversationId,
|
||||||
searchConversationName,
|
|
||||||
startSearchCounter: state.startSearchCounter + 1,
|
startSearchCounter: state.startSearchCounter + 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'CLEAR_CONVERSATION_SEARCH') {
|
if (action.type === 'CLEAR_CONVERSATION_SEARCH') {
|
||||||
const { searchConversationId, searchConversationName } = state;
|
const { searchConversationId } = state;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getEmptyState(),
|
...getEmptyState(),
|
||||||
searchConversationId,
|
searchConversationId,
|
||||||
searchConversationName,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'SEARCH_MESSAGES_RESULTS_FULFILLED') {
|
if (action.type === 'SEARCH_MESSAGES_RESULTS_FULFILLED') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { messages, normalizedPhoneNumber, query } = payload;
|
const { messages, query } = payload;
|
||||||
|
|
||||||
// Reject if the associated query is not the most recent user-provided query
|
// Reject if the associated query is not the most recent user-provided query
|
||||||
if (state.query !== query) {
|
if (state.query !== query) {
|
||||||
|
@ -415,7 +392,6 @@ export function reducer(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
normalizedPhoneNumber,
|
|
||||||
query,
|
query,
|
||||||
messageIds,
|
messageIds,
|
||||||
messageLookup: makeLookup(messages, 'id'),
|
messageLookup: makeLookup(messages, 'id'),
|
||||||
|
@ -425,7 +401,12 @@ export function reducer(
|
||||||
|
|
||||||
if (action.type === 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED') {
|
if (action.type === 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { contactIds, conversationIds } = payload;
|
const { contactIds, conversationIds, query } = payload;
|
||||||
|
|
||||||
|
// Reject if the associated query is not the most recent user-provided query
|
||||||
|
if (state.query !== query) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|
|
@ -21,7 +21,7 @@ import type {
|
||||||
import type { LeftPaneSearchPropsType } from '../../components/leftPane/LeftPaneSearchHelper';
|
import type { LeftPaneSearchPropsType } from '../../components/leftPane/LeftPaneSearchHelper';
|
||||||
import type { PropsDataType as MessageSearchResultPropsDataType } from '../../components/conversationList/MessageSearchResult';
|
import type { PropsDataType as MessageSearchResultPropsDataType } from '../../components/conversationList/MessageSearchResult';
|
||||||
|
|
||||||
import { getUserConversationId } from './user';
|
import { getIntl, getUserConversationId } from './user';
|
||||||
import type { GetConversationByIdType } from './conversations';
|
import type { GetConversationByIdType } from './conversations';
|
||||||
import {
|
import {
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
|
@ -30,6 +30,7 @@ import {
|
||||||
|
|
||||||
import type { BodyRangeType } from '../../types/Util';
|
import type { BodyRangeType } from '../../types/Util';
|
||||||
import * as log from '../../logging/log';
|
import * as log from '../../logging/log';
|
||||||
|
import { getOwn } from '../../util/getOwn';
|
||||||
|
|
||||||
export const getSearch = (state: StateType): SearchStateType => state.search;
|
export const getSearch = (state: StateType): SearchStateType => state.search;
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ export const getSelectedMessage = createSelector(
|
||||||
(state: SearchStateType): string | undefined => state.selectedMessage
|
(state: SearchStateType): string | undefined => state.selectedMessage
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getSearchConversationId = createSelector(
|
const getSearchConversationId = createSelector(
|
||||||
getSearch,
|
getSearch,
|
||||||
(state: SearchStateType): string | undefined => state.searchConversationId
|
(state: SearchStateType): string | undefined => state.searchConversationId
|
||||||
);
|
);
|
||||||
|
@ -53,9 +54,24 @@ export const getIsSearchingInAConversation = createSelector(
|
||||||
Boolean
|
Boolean
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getSearchConversation = createSelector(
|
||||||
|
getSearchConversationId,
|
||||||
|
getConversationLookup,
|
||||||
|
(searchConversationId, conversationLookup): undefined | ConversationType =>
|
||||||
|
searchConversationId
|
||||||
|
? getOwn(conversationLookup, searchConversationId)
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
|
||||||
export const getSearchConversationName = createSelector(
|
export const getSearchConversationName = createSelector(
|
||||||
getSearch,
|
getSearchConversation,
|
||||||
(state: SearchStateType): string | undefined => state.searchConversationName
|
getIntl,
|
||||||
|
(conversation, i18n): undefined | string => {
|
||||||
|
if (!conversation) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return conversation.isMe ? i18n('noteToSelf') : conversation.title;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getStartSearchCounter = createSelector(
|
export const getStartSearchCounter = createSelector(
|
||||||
|
@ -74,9 +90,10 @@ export const getMessageSearchResultLookup = createSelector(
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getSearchResults = createSelector(
|
export const getSearchResults = createSelector(
|
||||||
[getSearch, getConversationLookup],
|
[getSearch, getSearchConversationName, getConversationLookup],
|
||||||
(
|
(
|
||||||
state: SearchStateType,
|
state: SearchStateType,
|
||||||
|
searchConversationName,
|
||||||
conversationLookup: ConversationLookupType
|
conversationLookup: ConversationLookupType
|
||||||
): Omit<LeftPaneSearchPropsType, 'primarySendsSms'> => {
|
): Omit<LeftPaneSearchPropsType, 'primarySendsSms'> => {
|
||||||
const {
|
const {
|
||||||
|
@ -86,7 +103,6 @@ export const getSearchResults = createSelector(
|
||||||
messageIds,
|
messageIds,
|
||||||
messageLookup,
|
messageLookup,
|
||||||
messagesLoading,
|
messagesLoading,
|
||||||
searchConversationName,
|
|
||||||
} = state;
|
} = state;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { missingCaseError } from '../../util/missingCaseError';
|
||||||
import { ComposerStep, OneTimeModalState } from '../ducks/conversations';
|
import { ComposerStep, OneTimeModalState } from '../ducks/conversations';
|
||||||
import {
|
import {
|
||||||
getIsSearchingInAConversation,
|
getIsSearchingInAConversation,
|
||||||
|
getQuery,
|
||||||
|
getSearchConversation,
|
||||||
getSearchResults,
|
getSearchResults,
|
||||||
getStartSearchCounter,
|
getStartSearchCounter,
|
||||||
isSearching,
|
isSearching,
|
||||||
|
@ -91,9 +93,14 @@ const getModeSpecificProps = (
|
||||||
case undefined:
|
case undefined:
|
||||||
if (getShowArchived(state)) {
|
if (getShowArchived(state)) {
|
||||||
const { archivedConversations } = getLeftPaneLists(state);
|
const { archivedConversations } = getLeftPaneLists(state);
|
||||||
|
const searchConversation = getSearchConversation(state);
|
||||||
|
const searchTerm = getQuery(state);
|
||||||
return {
|
return {
|
||||||
mode: LeftPaneMode.Archive,
|
mode: LeftPaneMode.Archive,
|
||||||
archivedConversations,
|
archivedConversations,
|
||||||
|
searchConversation,
|
||||||
|
searchTerm,
|
||||||
|
...(searchConversation && searchTerm ? getSearchResults(state) : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isSearching(state)) {
|
if (isSearching(state)) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2019-2020 Signal Messenger, LLC
|
// Copyright 2019-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
@ -9,8 +9,7 @@ import type { StateType } from '../reducer';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getQuery,
|
getQuery,
|
||||||
getSearchConversationId,
|
getSearchConversation,
|
||||||
getSearchConversationName,
|
|
||||||
getStartSearchCounter,
|
getStartSearchCounter,
|
||||||
} from '../selectors/search';
|
} from '../selectors/search';
|
||||||
import {
|
import {
|
||||||
|
@ -27,8 +26,7 @@ const mapStateToProps = (state: StateType) => {
|
||||||
disabled: state.network.challengeStatus !== 'idle',
|
disabled: state.network.challengeStatus !== 'idle',
|
||||||
hasPendingUpdate: Boolean(state.updates.didSnooze),
|
hasPendingUpdate: Boolean(state.updates.didSnooze),
|
||||||
searchTerm: getQuery(state),
|
searchTerm: getQuery(state),
|
||||||
searchConversationId: getSearchConversationId(state),
|
searchConversation: getSearchConversation(state),
|
||||||
searchConversationName: getSearchConversationName(state),
|
|
||||||
selectedConversation: getSelectedConversation(state),
|
selectedConversation: getSelectedConversation(state),
|
||||||
startSearchCounter: getStartSearchCounter(state),
|
startSearchCounter: getStartSearchCounter(state),
|
||||||
regionCode: getRegionCode(state),
|
regionCode: getRegionCode(state),
|
||||||
|
|
|
@ -7,14 +7,41 @@ import { v4 as uuid } from 'uuid';
|
||||||
import { RowType } from '../../../components/ConversationList';
|
import { RowType } from '../../../components/ConversationList';
|
||||||
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
|
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
|
||||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||||
|
import { LeftPaneSearchHelper } from '../../../components/leftPane/LeftPaneSearchHelper';
|
||||||
|
|
||||||
import { LeftPaneArchiveHelper } from '../../../components/leftPane/LeftPaneArchiveHelper';
|
import { LeftPaneArchiveHelper } from '../../../components/leftPane/LeftPaneArchiveHelper';
|
||||||
|
|
||||||
describe('LeftPaneArchiveHelper', () => {
|
describe('LeftPaneArchiveHelper', () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
archivedConversations: [],
|
||||||
|
searchConversation: undefined,
|
||||||
|
searchTerm: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchingDefaults = {
|
||||||
|
...defaults,
|
||||||
|
searchConversation: getDefaultConversation(),
|
||||||
|
conversationResults: { isLoading: false, results: [] },
|
||||||
|
contactResults: { isLoading: false, results: [] },
|
||||||
|
messageResults: { isLoading: false, results: [] },
|
||||||
|
searchTerm: 'foo',
|
||||||
|
primarySendsSms: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
describe('getBackAction', () => {
|
describe('getBackAction', () => {
|
||||||
it('returns the "show inbox" action', () => {
|
it('returns the "show inbox" action', () => {
|
||||||
const showInbox = sinon.fake();
|
const showInbox = sinon.fake();
|
||||||
const helper = new LeftPaneArchiveHelper({ archivedConversations: [] });
|
const helper = new LeftPaneArchiveHelper(defaults);
|
||||||
|
|
||||||
assert.strictEqual(helper.getBackAction({ showInbox }), showInbox);
|
assert.strictEqual(helper.getBackAction({ showInbox }), showInbox);
|
||||||
});
|
});
|
||||||
|
@ -22,12 +49,10 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
|
|
||||||
describe('getRowCount', () => {
|
describe('getRowCount', () => {
|
||||||
it('returns the number of archived conversations', () => {
|
it('returns the number of archived conversations', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(new LeftPaneArchiveHelper(defaults).getRowCount(), 0);
|
||||||
new LeftPaneArchiveHelper({ archivedConversations: [] }).getRowCount(),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneArchiveHelper({
|
new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
archivedConversations: [
|
archivedConversations: [
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
|
@ -36,11 +61,20 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('defers to the search helper if searching', () => {
|
||||||
|
sandbox.stub(LeftPaneSearchHelper.prototype, 'getRowCount').returns(123);
|
||||||
|
assert.strictEqual(
|
||||||
|
new LeftPaneArchiveHelper(searchingDefaults).getRowCount(),
|
||||||
|
123
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getRowIndexToScrollTo', () => {
|
describe('getRowIndexToScrollTo', () => {
|
||||||
it('returns undefined if no conversation is selected', () => {
|
it('returns undefined if no conversation is selected', () => {
|
||||||
const helper = new LeftPaneArchiveHelper({
|
const helper = new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
archivedConversations: [
|
archivedConversations: [
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
|
@ -52,6 +86,7 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
|
|
||||||
it('returns undefined if the selected conversation is not pinned or non-pinned', () => {
|
it('returns undefined if the selected conversation is not pinned or non-pinned', () => {
|
||||||
const helper = new LeftPaneArchiveHelper({
|
const helper = new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
archivedConversations: [
|
archivedConversations: [
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
|
@ -66,7 +101,10 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
];
|
];
|
||||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
const helper = new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
|
archivedConversations,
|
||||||
|
});
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
helper.getRowIndexToScrollTo(archivedConversations[0].id),
|
helper.getRowIndexToScrollTo(archivedConversations[0].id),
|
||||||
|
@ -77,6 +115,23 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
1
|
1
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('defers to the search helper if searching', () => {
|
||||||
|
sandbox
|
||||||
|
.stub(LeftPaneSearchHelper.prototype, 'getRowIndexToScrollTo')
|
||||||
|
.returns(123);
|
||||||
|
|
||||||
|
const archivedConversations = [
|
||||||
|
getDefaultConversation(),
|
||||||
|
getDefaultConversation(),
|
||||||
|
];
|
||||||
|
const helper = new LeftPaneArchiveHelper(searchingDefaults);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
helper.getRowIndexToScrollTo(archivedConversations[0].id),
|
||||||
|
123
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getRow', () => {
|
describe('getRow', () => {
|
||||||
|
@ -85,7 +140,10 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
];
|
];
|
||||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
const helper = new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
|
archivedConversations,
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(helper.getRow(0), {
|
assert.deepEqual(helper.getRow(0), {
|
||||||
type: RowType.Conversation,
|
type: RowType.Conversation,
|
||||||
|
@ -96,6 +154,18 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
conversation: archivedConversations[1],
|
conversation: archivedConversations[1],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('defers to the search helper if searching', () => {
|
||||||
|
sandbox
|
||||||
|
.stub(LeftPaneSearchHelper.prototype, 'getRow')
|
||||||
|
.returns({ type: RowType.SearchResultsLoadingFakeHeader });
|
||||||
|
|
||||||
|
const helper = new LeftPaneArchiveHelper(searchingDefaults);
|
||||||
|
|
||||||
|
assert.deepEqual(helper.getRow(0), {
|
||||||
|
type: RowType.SearchResultsLoadingFakeHeader,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getConversationAndMessageAtIndex', () => {
|
describe('getConversationAndMessageAtIndex', () => {
|
||||||
|
@ -104,7 +174,10 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
];
|
];
|
||||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
const helper = new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
|
archivedConversations,
|
||||||
|
});
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
helper.getConversationAndMessageAtIndex(0)?.conversationId,
|
helper.getConversationAndMessageAtIndex(0)?.conversationId,
|
||||||
|
@ -121,7 +194,10 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
];
|
];
|
||||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
const helper = new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
|
archivedConversations,
|
||||||
|
});
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
helper.getConversationAndMessageAtIndex(2)?.conversationId,
|
helper.getConversationAndMessageAtIndex(2)?.conversationId,
|
||||||
|
@ -141,12 +217,27 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns undefined if there are no archived conversations', () => {
|
it('returns undefined if there are no archived conversations', () => {
|
||||||
const helper = new LeftPaneArchiveHelper({ archivedConversations: [] });
|
const helper = new LeftPaneArchiveHelper(defaults);
|
||||||
|
|
||||||
assert.isUndefined(helper.getConversationAndMessageAtIndex(0));
|
assert.isUndefined(helper.getConversationAndMessageAtIndex(0));
|
||||||
assert.isUndefined(helper.getConversationAndMessageAtIndex(1));
|
assert.isUndefined(helper.getConversationAndMessageAtIndex(1));
|
||||||
assert.isUndefined(helper.getConversationAndMessageAtIndex(-1));
|
assert.isUndefined(helper.getConversationAndMessageAtIndex(-1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('defers to the search helper if searching', () => {
|
||||||
|
sandbox
|
||||||
|
.stub(
|
||||||
|
LeftPaneSearchHelper.prototype,
|
||||||
|
'getConversationAndMessageAtIndex'
|
||||||
|
)
|
||||||
|
.returns({ conversationId: 'abc123' });
|
||||||
|
|
||||||
|
const helper = new LeftPaneArchiveHelper(searchingDefaults);
|
||||||
|
|
||||||
|
assert.deepEqual(helper.getConversationAndMessageAtIndex(999), {
|
||||||
|
conversationId: 'abc123',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getConversationAndMessageInDirection', () => {
|
describe('getConversationAndMessageInDirection', () => {
|
||||||
|
@ -155,7 +246,10 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
];
|
];
|
||||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
const helper = new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
|
archivedConversations,
|
||||||
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
helper.getConversationAndMessageInDirection(
|
helper.getConversationAndMessageInDirection(
|
||||||
|
@ -168,11 +262,37 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Additional tests are found with `getConversationInDirection`.
|
// Additional tests are found with `getConversationInDirection`.
|
||||||
|
|
||||||
|
it('defers to the search helper if searching', () => {
|
||||||
|
sandbox
|
||||||
|
.stub(
|
||||||
|
LeftPaneSearchHelper.prototype,
|
||||||
|
'getConversationAndMessageInDirection'
|
||||||
|
)
|
||||||
|
.returns({ conversationId: 'abc123' });
|
||||||
|
|
||||||
|
const helper = new LeftPaneArchiveHelper(searchingDefaults);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
helper.getConversationAndMessageInDirection(
|
||||||
|
{
|
||||||
|
direction: FindDirection.Down,
|
||||||
|
unreadOnly: false,
|
||||||
|
},
|
||||||
|
getDefaultConversation().id,
|
||||||
|
undefined
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conversationId: 'abc123',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('shouldRecomputeRowHeights', () => {
|
describe('shouldRecomputeRowHeights', () => {
|
||||||
it('always returns false because row heights are constant', () => {
|
it('returns false when not searching because row heights are constant', () => {
|
||||||
const helper = new LeftPaneArchiveHelper({
|
const helper = new LeftPaneArchiveHelper({
|
||||||
|
...defaults,
|
||||||
archivedConversations: [
|
archivedConversations: [
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
|
@ -181,11 +301,13 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
|
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
helper.shouldRecomputeRowHeights({
|
helper.shouldRecomputeRowHeights({
|
||||||
|
...defaults,
|
||||||
archivedConversations: [getDefaultConversation()],
|
archivedConversations: [getDefaultConversation()],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
helper.shouldRecomputeRowHeights({
|
helper.shouldRecomputeRowHeights({
|
||||||
|
...defaults,
|
||||||
archivedConversations: [
|
archivedConversations: [
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
getDefaultConversation(),
|
getDefaultConversation(),
|
||||||
|
@ -193,5 +315,27 @@ describe('LeftPaneArchiveHelper', () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns true when going from searching → not searching', () => {
|
||||||
|
const helper = new LeftPaneArchiveHelper(defaults);
|
||||||
|
|
||||||
|
assert.isTrue(helper.shouldRecomputeRowHeights(searchingDefaults));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true when going from not searching → searching', () => {
|
||||||
|
const helper = new LeftPaneArchiveHelper(searchingDefaults);
|
||||||
|
|
||||||
|
assert.isTrue(helper.shouldRecomputeRowHeights(defaults));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defers to the search helper if searching', () => {
|
||||||
|
sandbox
|
||||||
|
.stub(LeftPaneSearchHelper.prototype, 'shouldRecomputeRowHeights')
|
||||||
|
.returns(true);
|
||||||
|
|
||||||
|
const helper = new LeftPaneArchiveHelper(searchingDefaults);
|
||||||
|
|
||||||
|
assert.isTrue(helper.shouldRecomputeRowHeights(searchingDefaults));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -12660,6 +12660,14 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-07-21T18:34:59.251Z"
|
"updated": "2020-07-21T18:34:59.251Z"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/LeftPaneSearchInput.tsx",
|
||||||
|
"line": " const inputRef = useRef<null | HTMLInputElement>(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-10-29T22:48:58.354Z",
|
||||||
|
"reasonDetail": "Only used to focus the input."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/Lightbox.tsx",
|
"path": "ts/components/Lightbox.tsx",
|
||||||
|
|
|
@ -39,7 +39,6 @@ import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameC
|
||||||
import {
|
import {
|
||||||
isDirectConversation,
|
isDirectConversation,
|
||||||
isGroupV1,
|
isGroupV1,
|
||||||
isMe,
|
|
||||||
} from '../util/whatTypeOfConversation';
|
} from '../util/whatTypeOfConversation';
|
||||||
import { findAndFormatContact } from '../util/findAndFormatContact';
|
import { findAndFormatContact } from '../util/findAndFormatContact';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
|
@ -383,10 +382,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
||||||
onDeleteMessages: () => this.destroyMessages(),
|
onDeleteMessages: () => this.destroyMessages(),
|
||||||
onSearchInConversation: () => {
|
onSearchInConversation: () => {
|
||||||
const { searchInConversation } = window.reduxActions.search;
|
const { searchInConversation } = window.reduxActions.search;
|
||||||
const name = isMe(this.model.attributes)
|
searchInConversation(this.model.id);
|
||||||
? window.i18n('noteToSelf')
|
|
||||||
: this.model.getTitle();
|
|
||||||
searchInConversation(this.model.id, name);
|
|
||||||
},
|
},
|
||||||
onSetMuteNotifications: this.setMuteExpiration.bind(this),
|
onSetMuteNotifications: this.setMuteExpiration.bind(this),
|
||||||
onSetPin: this.setPin.bind(this),
|
onSetPin: this.setPin.bind(this),
|
||||||
|
|
|
@ -220,7 +220,7 @@ Whisper.InboxView = Whisper.View.extend({
|
||||||
view.remove();
|
view.remove();
|
||||||
|
|
||||||
const searchInput = document.querySelector(
|
const searchInput = document.querySelector(
|
||||||
'.module-main-header__search__input'
|
'.LeftPaneSearchInput__input'
|
||||||
) as HTMLElement;
|
) as HTMLElement;
|
||||||
searchInput?.focus?.();
|
searchInput?.focus?.();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue