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 {
|
||||
$icon: '../images/icons/v2/compose-outline-24.svg';
|
||||
|
||||
|
@ -5712,6 +5560,8 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
&__text {
|
||||
@include font-body-1-bold;
|
||||
flex-grow: 1;
|
||||
padding-right: 16px;
|
||||
|
||||
@include light-theme {
|
||||
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/Input.scss';
|
||||
@import './components/LeftPaneDialog.scss';
|
||||
@import './components/LeftPaneSearchInput.scss';
|
||||
@import './components/Lightbox.scss';
|
||||
@import './components/MediaQualitySelector.scss';
|
||||
@import './components/MessageAudio.scss';
|
||||
|
|
|
@ -1149,7 +1149,7 @@ export async function startApp(): Promise<void> {
|
|||
document.querySelector(
|
||||
'.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-left-pane__compose-search-form__input'
|
||||
|
@ -1228,8 +1228,8 @@ export async function startApp(): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
// MainHeader search box
|
||||
if (className.includes('module-main-header__search__input')) {
|
||||
// Search box
|
||||
if (className.includes('LeftPaneSearchInput__input')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export enum AvatarBlur {
|
|||
}
|
||||
|
||||
export enum AvatarSize {
|
||||
SIXTEEN = 16,
|
||||
TWENTY_EIGHT = 28,
|
||||
THIRTY_TWO = 32,
|
||||
THIRTY_SIX = 36,
|
||||
|
|
|
@ -83,6 +83,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
cantAddContactToGroup: action('cantAddContactToGroup'),
|
||||
canResizeLeftPane: true,
|
||||
clearGroupCreationError: action('clearGroupCreationError'),
|
||||
clearSearch: action('clearSearch'),
|
||||
closeCantAddContactToGroupModal: action('closeCantAddContactToGroupModal'),
|
||||
closeMaximumGroupSizeModal: action('closeMaximumGroupSizeModal'),
|
||||
closeRecommendedGroupSizeModal: action('closeRecommendedGroupSizeModal'),
|
||||
|
@ -131,6 +132,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
selectedConversationId: undefined,
|
||||
selectedMessageId: undefined,
|
||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
setComposeSearchTerm: action('setComposeSearchTerm'),
|
||||
setComposeGroupAvatar: action('setComposeGroupAvatar'),
|
||||
setComposeGroupName: action('setComposeGroupName'),
|
||||
|
@ -142,11 +144,13 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
startNewConversationFromPhoneNumber: action(
|
||||
'startNewConversationFromPhoneNumber'
|
||||
),
|
||||
startSearch: action('startSearch'),
|
||||
startSettingGroupMetadata: action('startSettingGroupMetadata'),
|
||||
toggleComposeEditingAvatar: action('toggleComposeEditingAvatar'),
|
||||
toggleConversationInChooseMembers: action(
|
||||
'toggleConversationInChooseMembers'
|
||||
),
|
||||
updateSearchTerm: action('updateSearchTerm'),
|
||||
|
||||
...overrideProps,
|
||||
});
|
||||
|
@ -393,6 +397,8 @@ story.add('Archive: no archived conversations', () => (
|
|||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Archive,
|
||||
archivedConversations: [],
|
||||
searchConversation: undefined,
|
||||
searchTerm: '',
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
@ -404,6 +410,25 @@ story.add('Archive: archived conversations', () => (
|
|||
modeSpecificProps: {
|
||||
mode: LeftPaneMode.Archive,
|
||||
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
|
||||
cantAddContactToGroup: (conversationId: string) => void;
|
||||
clearGroupCreationError: () => void;
|
||||
clearSearch: () => void;
|
||||
closeCantAddContactToGroupModal: () => void;
|
||||
closeMaximumGroupSizeModal: () => void;
|
||||
closeRecommendedGroupSizeModal: () => void;
|
||||
|
@ -105,6 +106,7 @@ export type PropsType = {
|
|||
switchToAssociatedView?: boolean;
|
||||
}) => void;
|
||||
savePreferredLeftPaneWidth: (_: number) => void;
|
||||
searchInConversation: (conversationId: string) => unknown;
|
||||
setComposeSearchTerm: (composeSearchTerm: string) => void;
|
||||
setComposeGroupAvatar: (_: undefined | Uint8Array) => void;
|
||||
setComposeGroupName: (_: string) => void;
|
||||
|
@ -112,6 +114,7 @@ export type PropsType = {
|
|||
showArchivedConversations: () => void;
|
||||
showInbox: () => void;
|
||||
startComposing: () => void;
|
||||
startSearch: () => unknown;
|
||||
showChooseGroupMembers: () => void;
|
||||
startSettingGroupMetadata: () => void;
|
||||
toggleConversationInChooseMembers: (conversationId: string) => void;
|
||||
|
@ -119,6 +122,7 @@ export type PropsType = {
|
|||
composeReplaceAvatar: ReplaceAvatarActionType;
|
||||
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
toggleComposeEditingAvatar: () => unknown;
|
||||
updateSearchTerm: (_: string) => void;
|
||||
|
||||
// Render Props
|
||||
renderExpiredBuildDialog: (
|
||||
|
@ -143,6 +147,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
canResizeLeftPane,
|
||||
challengeStatus,
|
||||
clearGroupCreationError,
|
||||
clearSearch,
|
||||
closeCantAddContactToGroupModal,
|
||||
closeMaximumGroupSizeModal,
|
||||
closeRecommendedGroupSizeModal,
|
||||
|
@ -162,6 +167,7 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
renderRelinkDialog,
|
||||
renderUpdateDialog,
|
||||
savePreferredLeftPaneWidth,
|
||||
searchInConversation,
|
||||
selectedConversationId,
|
||||
selectedMessageId,
|
||||
setChallengeStatus,
|
||||
|
@ -173,10 +179,12 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
showChooseGroupMembers,
|
||||
showInbox,
|
||||
startComposing,
|
||||
startSearch,
|
||||
startNewConversationFromPhoneNumber,
|
||||
startSettingGroupMetadata,
|
||||
toggleComposeEditingAvatar,
|
||||
toggleConversationInChooseMembers,
|
||||
updateSearchTerm,
|
||||
}) => {
|
||||
const [preferredWidth, setPreferredWidth] = useState(
|
||||
// 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.stopPropagation();
|
||||
}
|
||||
|
||||
helper.onKeyDown(event, {
|
||||
searchInConversation,
|
||||
selectedConversationId,
|
||||
startSearch,
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
|
@ -363,11 +377,13 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
}, [
|
||||
helper,
|
||||
openConversationInternal,
|
||||
searchInConversation,
|
||||
selectedConversationId,
|
||||
selectedMessageId,
|
||||
showChooseGroupMembers,
|
||||
showInbox,
|
||||
startComposing,
|
||||
startSearch,
|
||||
]);
|
||||
|
||||
const requiresFullWidth = helper.requiresFullWidth();
|
||||
|
@ -524,10 +540,12 @@ export const LeftPane: React.FC<PropsType> = ({
|
|||
{/* eslint-enable jsx-a11y/no-static-element-interactions */}
|
||||
<div className="module-left-pane__header">
|
||||
{helper.getHeaderContents({
|
||||
clearSearch,
|
||||
i18n,
|
||||
showInbox,
|
||||
startComposing,
|
||||
showChooseGroupMembers,
|
||||
updateSearchTerm,
|
||||
}) || renderMainHeader()}
|
||||
</div>
|
||||
{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 { storiesOf } from '@storybook/react';
|
||||
import { text, withKnobs } from '@storybook/addon-knobs';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { PropsType } from './MainHeader';
|
||||
import { MainHeader } from './MainHeader';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
|
@ -20,28 +21,12 @@ const requiredText = (name: string, value: string | undefined) =>
|
|||
const optionalText = (name: string, value: string | 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 => ({
|
||||
searchTerm: requiredText('searchTerm', overrideProps.searchTerm),
|
||||
searchConversationName: optionalText(
|
||||
'searchConversationName',
|
||||
overrideProps.searchConversationName
|
||||
),
|
||||
searchConversationId: optionalText(
|
||||
'searchConversationId',
|
||||
overrideProps.searchConversationId
|
||||
),
|
||||
searchConversation: overrideProps.searchConversation,
|
||||
selectedConversation: undefined,
|
||||
startSearchCounter: 0,
|
||||
|
||||
ourConversationId: '',
|
||||
ourUuid: '',
|
||||
ourNumber: '',
|
||||
regionCode: '',
|
||||
|
||||
phoneNumber: optionalText('phoneNumber', overrideProps.phoneNumber),
|
||||
title: requiredText('title', overrideProps.title),
|
||||
name: optionalText('name', overrideProps.name),
|
||||
|
@ -51,10 +36,6 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
i18n,
|
||||
|
||||
updateSearchTerm: action('updateSearchTerm'),
|
||||
searchMessages: action('searchMessages'),
|
||||
searchDiscussions: action('searchDiscussions'),
|
||||
startSearch: action('startSearch'),
|
||||
searchInConversation: action('searchInConversation'),
|
||||
clearConversationSearch: action('clearConversationSearch'),
|
||||
clearSearch: action('clearSearch'),
|
||||
startUpdate: action('startUpdate'),
|
||||
|
@ -101,8 +82,7 @@ story.add('Search Term', () => {
|
|||
story.add('Searching Conversation', () => {
|
||||
const props = createProps({
|
||||
name: 'John Smith',
|
||||
searchConversationId: 'group-id-1',
|
||||
searchConversationName: 'Everyone',
|
||||
searchConversation: getDefaultConversation(),
|
||||
});
|
||||
|
||||
return <MainHeader {...props} />;
|
||||
|
@ -112,8 +92,7 @@ story.add('Searching Conversation with Term', () => {
|
|||
const props = createProps({
|
||||
name: 'John Smith',
|
||||
searchTerm: 'address',
|
||||
searchConversationId: 'group-id-1',
|
||||
searchConversationName: 'Everyone',
|
||||
searchConversation: getDefaultConversation(),
|
||||
});
|
||||
|
||||
return <MainHeader {...props} />;
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { debounce, get } from 'lodash';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
|
@ -13,20 +11,14 @@ import { AvatarPopup } from './AvatarPopup';
|
|||
import type { LocalizerType } from '../types/Util';
|
||||
import type { AvatarColorType } from '../types/Colors';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { LeftPaneSearchInput } from './LeftPaneSearchInput';
|
||||
|
||||
export type PropsType = {
|
||||
searchTerm: string;
|
||||
searchConversationName?: string;
|
||||
searchConversationId?: string;
|
||||
searchConversation: undefined | ConversationType;
|
||||
startSearchCounter: number;
|
||||
selectedConversation: undefined | ConversationType;
|
||||
|
||||
// To be used as an ID
|
||||
ourConversationId: string;
|
||||
ourUuid: string;
|
||||
ourNumber: string;
|
||||
regionCode: string;
|
||||
|
||||
// For display
|
||||
phoneNumber?: string;
|
||||
isMe?: boolean;
|
||||
|
@ -42,24 +34,6 @@ export type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
|
||||
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;
|
||||
clearConversationSearch: () => void;
|
||||
clearSearch: () => void;
|
||||
|
@ -89,12 +63,12 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
|
||||
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
|
||||
if (
|
||||
searchConversationId &&
|
||||
searchConversationId !== prevProps.searchConversationId
|
||||
searchConversation &&
|
||||
searchConversation.id !== prevProps.searchConversation?.id
|
||||
) {
|
||||
this.setFocus();
|
||||
}
|
||||
|
@ -157,46 +131,16 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
}
|
||||
}
|
||||
|
||||
public search = debounce((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 => {
|
||||
private updateSearch = (searchTerm: string): void => {
|
||||
const {
|
||||
updateSearchTerm,
|
||||
clearConversationSearch,
|
||||
clearSearch,
|
||||
searchConversationId,
|
||||
searchConversation,
|
||||
} = this.props;
|
||||
const searchTerm = event.currentTarget.value;
|
||||
|
||||
if (!searchTerm) {
|
||||
if (searchConversationId) {
|
||||
if (searchConversation) {
|
||||
clearConversationSearch();
|
||||
} else {
|
||||
clearSearch();
|
||||
|
@ -208,132 +152,30 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
if (updateSearchTerm) {
|
||||
updateSearchTerm(searchTerm);
|
||||
}
|
||||
|
||||
if (searchTerm.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.search(searchTerm);
|
||||
};
|
||||
|
||||
public clearSearch = (): void => {
|
||||
const { clearSearch } = this.props;
|
||||
|
||||
clearSearch();
|
||||
this.setFocus();
|
||||
};
|
||||
|
||||
public clearConversationSearch = (): void => {
|
||||
const { clearConversationSearch } = this.props;
|
||||
|
||||
clearConversationSearch();
|
||||
this.setFocus();
|
||||
};
|
||||
|
||||
private handleInputBlur = (): void => {
|
||||
const { clearSearch, searchConversationId, searchTerm } = this.props;
|
||||
if (!searchConversationId && !searchTerm) {
|
||||
const { clearSearch, searchConversation, searchTerm } = this.props;
|
||||
if (!searchConversation && !searchTerm) {
|
||||
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 => {
|
||||
const { showingAvatarPopup } = this.state;
|
||||
const {
|
||||
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;
|
||||
const { key } = event;
|
||||
|
||||
if (showingAvatarPopup && key === 'Escape') {
|
||||
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 => {
|
||||
if (this.inputRef.current) {
|
||||
this.inputRef.current.focus();
|
||||
|
@ -356,8 +198,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
searchConversation,
|
||||
searchTerm,
|
||||
showArchivedConversations,
|
||||
startComposing,
|
||||
|
@ -367,13 +208,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
} = this.props;
|
||||
const { showingAvatarPopup, popperRoot } = this.state;
|
||||
|
||||
const placeholder = searchConversationName
|
||||
? i18n('searchIn', [searchConversationName])
|
||||
: i18n('search');
|
||||
|
||||
const isSearching = Boolean(
|
||||
searchConversationId || searchTerm.trim().length
|
||||
);
|
||||
const isSearching = Boolean(searchConversation || searchTerm.trim().length);
|
||||
|
||||
return (
|
||||
<div className="module-main-header">
|
||||
|
@ -447,59 +282,16 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
)
|
||||
: null}
|
||||
</Manager>
|
||||
<div className="module-main-header__search">
|
||||
{searchConversationId ? (
|
||||
<button
|
||||
className="module-main-header__search__in-conversation-pill"
|
||||
onClick={this.clearSearch}
|
||||
tabIndex={-1}
|
||||
type="button"
|
||||
aria-label={i18n('clearSearch')}
|
||||
>
|
||||
<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>
|
||||
<LeftPaneSearchInput
|
||||
disabled={disabled}
|
||||
i18n={i18n}
|
||||
onBlur={this.handleInputBlur}
|
||||
onChangeValue={this.updateSearch}
|
||||
onClear={this.clearSearch}
|
||||
ref={this.inputRef}
|
||||
searchConversation={searchConversation}
|
||||
value={searchTerm}
|
||||
/>
|
||||
{!isSearching && (
|
||||
<button
|
||||
aria-label={i18n('newConversation')}
|
||||
|
|
|
@ -97,7 +97,7 @@ type ActionProps = {
|
|||
saveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
showContactModal: (contactId: string, conversationId: string) => void;
|
||||
toggleSafetyNumberModal: (conversationId: string) => unknown;
|
||||
searchInConversation: (id: string, title: string) => unknown;
|
||||
searchInConversation: (id: string) => unknown;
|
||||
};
|
||||
|
||||
export type Props = StateProps & ActionProps;
|
||||
|
@ -365,10 +365,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
|||
<Button
|
||||
icon={ButtonIconType.search}
|
||||
onClick={() => {
|
||||
searchInConversation(
|
||||
conversation.id,
|
||||
conversation.isMe ? i18n('noteToSelf') : conversation.title
|
||||
);
|
||||
searchInConversation(conversation.id);
|
||||
}}
|
||||
variant={ButtonVariant.Details}
|
||||
>
|
||||
|
|
|
@ -12,28 +12,54 @@ import type { Row } from '../ConversationList';
|
|||
import { RowType } from '../ConversationList';
|
||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||
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>;
|
||||
searchConversation: undefined | ConversationType;
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export type LeftPaneArchivePropsType =
|
||||
| LeftPaneArchiveBasePropsType
|
||||
| (LeftPaneArchiveBasePropsType & LeftPaneSearchPropsType);
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsType> {
|
||||
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();
|
||||
|
||||
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({
|
||||
clearSearch,
|
||||
i18n,
|
||||
showInbox,
|
||||
updateSearchTerm,
|
||||
}: Readonly<{
|
||||
clearSearch: () => void;
|
||||
i18n: LocalizerType;
|
||||
showInbox: () => void;
|
||||
updateSearchTerm: (query: string) => void;
|
||||
}>): ReactChild {
|
||||
return (
|
||||
<div className="module-left-pane__header__contents">
|
||||
|
@ -45,7 +71,24 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
|||
type="button"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
|
@ -55,7 +98,13 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
|||
return showInbox;
|
||||
}
|
||||
|
||||
getPreRowsNode({ i18n }: Readonly<{ i18n: LocalizerType }>): ReactChild {
|
||||
getPreRowsNode({
|
||||
i18n,
|
||||
}: Readonly<{ i18n: LocalizerType }>): ReactChild | null {
|
||||
if (this.searchHelper) {
|
||||
return this.searchHelper.getPreRowsNode({ i18n });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-left-pane__archive-helper-text">
|
||||
{i18n('archiveHelperText')}
|
||||
|
@ -64,10 +113,16 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
|||
}
|
||||
|
||||
getRowCount(): number {
|
||||
return this.archivedConversations.length;
|
||||
return (
|
||||
this.searchHelper?.getRowCount() ?? this.archivedConversations.length
|
||||
);
|
||||
}
|
||||
|
||||
getRow(rowIndex: number): undefined | Row {
|
||||
if (this.searchHelper) {
|
||||
return this.searchHelper.getRow(rowIndex);
|
||||
}
|
||||
|
||||
const conversation = this.archivedConversations[rowIndex];
|
||||
return conversation
|
||||
? {
|
||||
|
@ -80,6 +135,10 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
|||
getRowIndexToScrollTo(
|
||||
selectedConversationId: undefined | string
|
||||
): undefined | number {
|
||||
if (this.searchHelper) {
|
||||
return this.searchHelper.getRowIndexToScrollTo(selectedConversationId);
|
||||
}
|
||||
|
||||
if (!selectedConversationId) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -92,7 +151,12 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
|||
getConversationAndMessageAtIndex(
|
||||
conversationIndex: number
|
||||
): undefined | { conversationId: string } {
|
||||
const { archivedConversations } = this;
|
||||
const { archivedConversations, searchHelper } = this;
|
||||
|
||||
if (searchHelper) {
|
||||
return searchHelper.getConversationAndMessageAtIndex(conversationIndex);
|
||||
}
|
||||
|
||||
const conversation =
|
||||
archivedConversations[conversationIndex] || last(archivedConversations);
|
||||
return conversation ? { conversationId: conversation.id } : undefined;
|
||||
|
@ -101,8 +165,16 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
|||
getConversationAndMessageInDirection(
|
||||
toFind: Readonly<ToFindType>,
|
||||
selectedConversationId: undefined | string,
|
||||
_selectedMessageId: unknown
|
||||
selectedMessageId: unknown
|
||||
): undefined | { conversationId: string } {
|
||||
if (this.searchHelper) {
|
||||
return this.searchHelper.getConversationAndMessageInDirection(
|
||||
toFind,
|
||||
selectedConversationId,
|
||||
selectedMessageId
|
||||
);
|
||||
}
|
||||
|
||||
return getConversationInDirection(
|
||||
this.archivedConversations,
|
||||
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;
|
||||
}
|
||||
|
||||
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> {
|
||||
getHeaderContents(
|
||||
_: Readonly<{
|
||||
clearSearch: () => void;
|
||||
i18n: LocalizerType;
|
||||
showInbox: () => void;
|
||||
startComposing: () => void;
|
||||
showChooseGroupMembers: () => void;
|
||||
updateSearchTerm: (query: string) => void;
|
||||
}>
|
||||
): null | ReactChild {
|
||||
return null;
|
||||
|
@ -97,6 +99,17 @@ export abstract class LeftPaneHelper<T> {
|
|||
return true;
|
||||
}
|
||||
|
||||
onKeyDown(
|
||||
_event: KeyboardEvent,
|
||||
_options: Readonly<{
|
||||
searchInConversation: (conversationId: string) => unknown;
|
||||
selectedConversationId: undefined | string;
|
||||
startSearch: () => unknown;
|
||||
}>
|
||||
): void {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
abstract getConversationAndMessageAtIndex(
|
||||
conversationIndex: number
|
||||
): undefined | { conversationId: string; messageId?: string };
|
||||
|
|
|
@ -13,6 +13,7 @@ import type { Row } from '../ConversationList';
|
|||
import { RowType } from '../ConversationList';
|
||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { handleKeydownForSearch } from './handleKeydownForSearch';
|
||||
|
||||
export type LeftPaneInboxPropsType = {
|
||||
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 {
|
||||
return Boolean(
|
||||
this.pinnedConversations.length && this.conversations.length
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { LocalizerType } from '../../types/Util';
|
|||
import type { Row } from '../ConversationList';
|
||||
import { RowType } from '../ConversationList';
|
||||
import type { PropsData as ConversationListItemPropsType } from '../conversationList/ConversationListItem';
|
||||
import { handleKeydownForSearch } from './handleKeydownForSearch';
|
||||
|
||||
import { Intl } from '../Intl';
|
||||
import { Emojify } from '../conversation/Emojify';
|
||||
|
@ -42,6 +43,8 @@ const searchResultKeys: Array<
|
|||
'conversationResults' | 'contactResults' | 'messageResults'
|
||||
> = ['conversationResults', 'contactResults', 'messageResults'];
|
||||
|
||||
/* eslint-disable class-methods-use-this */
|
||||
|
||||
export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType> {
|
||||
private readonly conversationResults: MaybeLoadedSearchResultsType<ConversationListItemPropsType>;
|
||||
|
||||
|
@ -270,6 +273,17 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
|||
return undefined;
|
||||
}
|
||||
|
||||
onKeyDown(
|
||||
event: KeyboardEvent,
|
||||
options: Readonly<{
|
||||
searchInConversation: (conversationId: string) => unknown;
|
||||
selectedConversationId: undefined | string;
|
||||
startSearch: () => unknown;
|
||||
}>
|
||||
): void {
|
||||
handleKeydownForSearch(event, options);
|
||||
}
|
||||
|
||||
private allResults() {
|
||||
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
|
||||
|
||||
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 type {
|
||||
ClientSearchResultMessageType,
|
||||
|
@ -21,6 +22,8 @@ import type {
|
|||
SelectedConversationChangedActionType,
|
||||
ShowArchivedConversationsActionType,
|
||||
} from './conversations';
|
||||
import { getQuery, getSearchConversation } from '../selectors/search';
|
||||
import { getIntl, getUserConversationId } from '../selectors/user';
|
||||
|
||||
const {
|
||||
searchConversations: dataSearchConversations,
|
||||
|
@ -41,11 +44,9 @@ export type MessageSearchResultLookupType = {
|
|||
export type SearchStateType = {
|
||||
startSearchCounter: number;
|
||||
searchConversationId?: string;
|
||||
searchConversationName?: string;
|
||||
contactIds: Array<string>;
|
||||
conversationIds: Array<string>;
|
||||
query: string;
|
||||
normalizedPhoneNumber?: string;
|
||||
messageIds: Array<string>;
|
||||
// We do store message data to pass through the selector
|
||||
messageLookup: MessageSearchResultLookupType;
|
||||
|
@ -57,33 +58,20 @@ export type SearchStateType = {
|
|||
|
||||
// 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: 'SEARCH_MESSAGES_RESULTS_FULFILLED';
|
||||
payload: SearchMessagesResultsPayloadType;
|
||||
payload: {
|
||||
messages: Array<MessageSearchResultType>;
|
||||
query: string;
|
||||
};
|
||||
};
|
||||
type SearchDiscussionsResultsFulfilledActionType = {
|
||||
type: 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED';
|
||||
payload: SearchDiscussionsResultsPayloadType;
|
||||
payload: {
|
||||
conversationIds: Array<string>;
|
||||
contactIds: Array<string>;
|
||||
query: string;
|
||||
};
|
||||
};
|
||||
type UpdateSearchTermActionType = {
|
||||
type: 'SEARCH_UPDATE';
|
||||
|
@ -105,15 +93,10 @@ type ClearConversationSearchActionType = {
|
|||
};
|
||||
type SearchInConversationActionType = {
|
||||
type: 'SEARCH_IN_CONVERSATION';
|
||||
payload: {
|
||||
searchConversationId: string;
|
||||
searchConversationName: string;
|
||||
};
|
||||
payload: { searchConversationId: string };
|
||||
};
|
||||
|
||||
export type SearchActionType =
|
||||
| SearchMessagesResultsKickoffActionType
|
||||
| SearchDiscussionsResultsKickoffActionType
|
||||
| SearchMessagesResultsFulfilledActionType
|
||||
| SearchDiscussionsResultsFulfilledActionType
|
||||
| UpdateSearchTermActionType
|
||||
|
@ -130,8 +113,6 @@ export type SearchActionType =
|
|||
// Action Creators
|
||||
|
||||
export const actions = {
|
||||
searchMessages,
|
||||
searchDiscussions,
|
||||
startSearch,
|
||||
clearSearch,
|
||||
clearConversationSearch,
|
||||
|
@ -139,72 +120,6 @@ export const actions = {
|
|||
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 {
|
||||
return {
|
||||
type: 'SEARCH_START',
|
||||
|
@ -224,27 +139,92 @@ function clearConversationSearch(): ClearConversationSearchActionType {
|
|||
};
|
||||
}
|
||||
function searchInConversation(
|
||||
searchConversationId: string,
|
||||
searchConversationName: string
|
||||
searchConversationId: string
|
||||
): SearchInConversationActionType {
|
||||
return {
|
||||
type: 'SEARCH_IN_CONVERSATION',
|
||||
payload: {
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
},
|
||||
payload: { searchConversationId },
|
||||
};
|
||||
}
|
||||
|
||||
function updateSearchTerm(query: string): UpdateSearchTermActionType {
|
||||
return {
|
||||
type: 'SEARCH_UPDATE',
|
||||
payload: {
|
||||
query,
|
||||
},
|
||||
function updateSearchTerm(
|
||||
query: string
|
||||
): ThunkAction<void, RootStateType, unknown, UpdateSearchTermActionType> {
|
||||
return (dispatch, getState) => {
|
||||
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(
|
||||
query: string,
|
||||
searchConversationId?: string
|
||||
|
@ -342,7 +322,6 @@ export function reducer(
|
|||
return {
|
||||
...state,
|
||||
searchConversationId: undefined,
|
||||
searchConversationName: undefined,
|
||||
startSearchCounter: state.startSearchCounter + 1,
|
||||
};
|
||||
}
|
||||
|
@ -376,7 +355,7 @@ export function reducer(
|
|||
|
||||
if (action.type === 'SEARCH_IN_CONVERSATION') {
|
||||
const { payload } = action;
|
||||
const { searchConversationId, searchConversationName } = payload;
|
||||
const { searchConversationId } = payload;
|
||||
|
||||
if (searchConversationId === state.searchConversationId) {
|
||||
return {
|
||||
|
@ -388,23 +367,21 @@ export function reducer(
|
|||
return {
|
||||
...getEmptyState(),
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
startSearchCounter: state.startSearchCounter + 1,
|
||||
};
|
||||
}
|
||||
if (action.type === 'CLEAR_CONVERSATION_SEARCH') {
|
||||
const { searchConversationId, searchConversationName } = state;
|
||||
const { searchConversationId } = state;
|
||||
|
||||
return {
|
||||
...getEmptyState(),
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === 'SEARCH_MESSAGES_RESULTS_FULFILLED') {
|
||||
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
|
||||
if (state.query !== query) {
|
||||
|
@ -415,7 +392,6 @@ export function reducer(
|
|||
|
||||
return {
|
||||
...state,
|
||||
normalizedPhoneNumber,
|
||||
query,
|
||||
messageIds,
|
||||
messageLookup: makeLookup(messages, 'id'),
|
||||
|
@ -425,7 +401,12 @@ export function reducer(
|
|||
|
||||
if (action.type === 'SEARCH_DISCUSSIONS_RESULTS_FULFILLED') {
|
||||
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 {
|
||||
...state,
|
||||
|
|
|
@ -21,7 +21,7 @@ import type {
|
|||
import type { LeftPaneSearchPropsType } from '../../components/leftPane/LeftPaneSearchHelper';
|
||||
import type { PropsDataType as MessageSearchResultPropsDataType } from '../../components/conversationList/MessageSearchResult';
|
||||
|
||||
import { getUserConversationId } from './user';
|
||||
import { getIntl, getUserConversationId } from './user';
|
||||
import type { GetConversationByIdType } from './conversations';
|
||||
import {
|
||||
getConversationLookup,
|
||||
|
@ -30,6 +30,7 @@ import {
|
|||
|
||||
import type { BodyRangeType } from '../../types/Util';
|
||||
import * as log from '../../logging/log';
|
||||
import { getOwn } from '../../util/getOwn';
|
||||
|
||||
export const getSearch = (state: StateType): SearchStateType => state.search;
|
||||
|
||||
|
@ -43,7 +44,7 @@ export const getSelectedMessage = createSelector(
|
|||
(state: SearchStateType): string | undefined => state.selectedMessage
|
||||
);
|
||||
|
||||
export const getSearchConversationId = createSelector(
|
||||
const getSearchConversationId = createSelector(
|
||||
getSearch,
|
||||
(state: SearchStateType): string | undefined => state.searchConversationId
|
||||
);
|
||||
|
@ -53,9 +54,24 @@ export const getIsSearchingInAConversation = createSelector(
|
|||
Boolean
|
||||
);
|
||||
|
||||
export const getSearchConversation = createSelector(
|
||||
getSearchConversationId,
|
||||
getConversationLookup,
|
||||
(searchConversationId, conversationLookup): undefined | ConversationType =>
|
||||
searchConversationId
|
||||
? getOwn(conversationLookup, searchConversationId)
|
||||
: undefined
|
||||
);
|
||||
|
||||
export const getSearchConversationName = createSelector(
|
||||
getSearch,
|
||||
(state: SearchStateType): string | undefined => state.searchConversationName
|
||||
getSearchConversation,
|
||||
getIntl,
|
||||
(conversation, i18n): undefined | string => {
|
||||
if (!conversation) {
|
||||
return undefined;
|
||||
}
|
||||
return conversation.isMe ? i18n('noteToSelf') : conversation.title;
|
||||
}
|
||||
);
|
||||
|
||||
export const getStartSearchCounter = createSelector(
|
||||
|
@ -74,9 +90,10 @@ export const getMessageSearchResultLookup = createSelector(
|
|||
);
|
||||
|
||||
export const getSearchResults = createSelector(
|
||||
[getSearch, getConversationLookup],
|
||||
[getSearch, getSearchConversationName, getConversationLookup],
|
||||
(
|
||||
state: SearchStateType,
|
||||
searchConversationName,
|
||||
conversationLookup: ConversationLookupType
|
||||
): Omit<LeftPaneSearchPropsType, 'primarySendsSms'> => {
|
||||
const {
|
||||
|
@ -86,7 +103,6 @@ export const getSearchResults = createSelector(
|
|||
messageIds,
|
||||
messageLookup,
|
||||
messagesLoading,
|
||||
searchConversationName,
|
||||
} = state;
|
||||
|
||||
return {
|
||||
|
|
|
@ -13,6 +13,8 @@ import { missingCaseError } from '../../util/missingCaseError';
|
|||
import { ComposerStep, OneTimeModalState } from '../ducks/conversations';
|
||||
import {
|
||||
getIsSearchingInAConversation,
|
||||
getQuery,
|
||||
getSearchConversation,
|
||||
getSearchResults,
|
||||
getStartSearchCounter,
|
||||
isSearching,
|
||||
|
@ -91,9 +93,14 @@ const getModeSpecificProps = (
|
|||
case undefined:
|
||||
if (getShowArchived(state)) {
|
||||
const { archivedConversations } = getLeftPaneLists(state);
|
||||
const searchConversation = getSearchConversation(state);
|
||||
const searchTerm = getQuery(state);
|
||||
return {
|
||||
mode: LeftPaneMode.Archive,
|
||||
archivedConversations,
|
||||
searchConversation,
|
||||
searchTerm,
|
||||
...(searchConversation && searchTerm ? getSearchResults(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
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
@ -9,8 +9,7 @@ import type { StateType } from '../reducer';
|
|||
|
||||
import {
|
||||
getQuery,
|
||||
getSearchConversationId,
|
||||
getSearchConversationName,
|
||||
getSearchConversation,
|
||||
getStartSearchCounter,
|
||||
} from '../selectors/search';
|
||||
import {
|
||||
|
@ -27,8 +26,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
disabled: state.network.challengeStatus !== 'idle',
|
||||
hasPendingUpdate: Boolean(state.updates.didSnooze),
|
||||
searchTerm: getQuery(state),
|
||||
searchConversationId: getSearchConversationId(state),
|
||||
searchConversationName: getSearchConversationName(state),
|
||||
searchConversation: getSearchConversation(state),
|
||||
selectedConversation: getSelectedConversation(state),
|
||||
startSearchCounter: getStartSearchCounter(state),
|
||||
regionCode: getRegionCode(state),
|
||||
|
|
|
@ -7,14 +7,41 @@ import { v4 as uuid } from 'uuid';
|
|||
import { RowType } from '../../../components/ConversationList';
|
||||
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
|
||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||
import { LeftPaneSearchHelper } from '../../../components/leftPane/LeftPaneSearchHelper';
|
||||
|
||||
import { LeftPaneArchiveHelper } from '../../../components/leftPane/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', () => {
|
||||
it('returns the "show inbox" action', () => {
|
||||
const showInbox = sinon.fake();
|
||||
const helper = new LeftPaneArchiveHelper({ archivedConversations: [] });
|
||||
const helper = new LeftPaneArchiveHelper(defaults);
|
||||
|
||||
assert.strictEqual(helper.getBackAction({ showInbox }), showInbox);
|
||||
});
|
||||
|
@ -22,12 +49,10 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
|
||||
describe('getRowCount', () => {
|
||||
it('returns the number of archived conversations', () => {
|
||||
assert.strictEqual(
|
||||
new LeftPaneArchiveHelper({ archivedConversations: [] }).getRowCount(),
|
||||
0
|
||||
);
|
||||
assert.strictEqual(new LeftPaneArchiveHelper(defaults).getRowCount(), 0);
|
||||
assert.strictEqual(
|
||||
new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -36,11 +61,20 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
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', () => {
|
||||
it('returns undefined if no conversation is selected', () => {
|
||||
const helper = new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -52,6 +86,7 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
|
||||
it('returns undefined if the selected conversation is not pinned or non-pinned', () => {
|
||||
const helper = new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -66,7 +101,10 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
||||
const helper = new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
helper.getRowIndexToScrollTo(archivedConversations[0].id),
|
||||
|
@ -77,6 +115,23 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
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', () => {
|
||||
|
@ -85,7 +140,10 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
||||
const helper = new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations,
|
||||
});
|
||||
|
||||
assert.deepEqual(helper.getRow(0), {
|
||||
type: RowType.Conversation,
|
||||
|
@ -96,6 +154,18 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
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', () => {
|
||||
|
@ -104,7 +174,10 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
||||
const helper = new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
helper.getConversationAndMessageAtIndex(0)?.conversationId,
|
||||
|
@ -121,7 +194,10 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
||||
const helper = new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations,
|
||||
});
|
||||
|
||||
assert.strictEqual(
|
||||
helper.getConversationAndMessageAtIndex(2)?.conversationId,
|
||||
|
@ -141,12 +217,27 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
});
|
||||
|
||||
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(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', () => {
|
||||
|
@ -155,7 +246,10 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
];
|
||||
const helper = new LeftPaneArchiveHelper({ archivedConversations });
|
||||
const helper = new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
helper.getConversationAndMessageInDirection(
|
||||
|
@ -168,11 +262,37 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
});
|
||||
|
||||
// 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', () => {
|
||||
it('always returns false because row heights are constant', () => {
|
||||
it('returns false when not searching because row heights are constant', () => {
|
||||
const helper = new LeftPaneArchiveHelper({
|
||||
...defaults,
|
||||
archivedConversations: [
|
||||
getDefaultConversation(),
|
||||
getDefaultConversation(),
|
||||
|
@ -181,11 +301,13 @@ describe('LeftPaneArchiveHelper', () => {
|
|||
|
||||
assert.isFalse(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
...defaults,
|
||||
archivedConversations: [getDefaultConversation()],
|
||||
})
|
||||
);
|
||||
assert.isFalse(
|
||||
helper.shouldRecomputeRowHeights({
|
||||
...defaults,
|
||||
archivedConversations: [
|
||||
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",
|
||||
"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",
|
||||
"path": "ts/components/Lightbox.tsx",
|
||||
|
|
|
@ -39,7 +39,6 @@ import type { GroupNameCollisionsWithIdsByTitle } from '../util/groupMemberNameC
|
|||
import {
|
||||
isDirectConversation,
|
||||
isGroupV1,
|
||||
isMe,
|
||||
} from '../util/whatTypeOfConversation';
|
||||
import { findAndFormatContact } from '../util/findAndFormatContact';
|
||||
import * as Bytes from '../Bytes';
|
||||
|
@ -383,10 +382,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
onDeleteMessages: () => this.destroyMessages(),
|
||||
onSearchInConversation: () => {
|
||||
const { searchInConversation } = window.reduxActions.search;
|
||||
const name = isMe(this.model.attributes)
|
||||
? window.i18n('noteToSelf')
|
||||
: this.model.getTitle();
|
||||
searchInConversation(this.model.id, name);
|
||||
searchInConversation(this.model.id);
|
||||
},
|
||||
onSetMuteNotifications: this.setMuteExpiration.bind(this),
|
||||
onSetPin: this.setPin.bind(this),
|
||||
|
|
|
@ -220,7 +220,7 @@ Whisper.InboxView = Whisper.View.extend({
|
|||
view.remove();
|
||||
|
||||
const searchInput = document.querySelector(
|
||||
'.module-main-header__search__input'
|
||||
'.LeftPaneSearchInput__input'
|
||||
) as HTMLElement;
|
||||
searchInput?.focus?.();
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue