Filter chats by Unread
This commit is contained in:
parent
45e9c07125
commit
a56e7d0ade
27 changed files with 883 additions and 438 deletions
|
@ -927,6 +927,10 @@
|
||||||
"messageformat": "Search",
|
"messageformat": "Search",
|
||||||
"description": "Placeholder text in the search input"
|
"description": "Placeholder text in the search input"
|
||||||
},
|
},
|
||||||
|
"icu:searchUnreadChats": {
|
||||||
|
"messageformat": "Search unread chats",
|
||||||
|
"description": "Placeholder text in the search input when unread filter is enabled"
|
||||||
|
},
|
||||||
"icu:clearSearch": {
|
"icu:clearSearch": {
|
||||||
"messageformat": "Clear Search",
|
"messageformat": "Clear Search",
|
||||||
"description": "Aria label for clear search button"
|
"description": "Aria label for clear search button"
|
||||||
|
@ -939,6 +943,14 @@
|
||||||
"messageformat": "No results for \"{searchTerm}\"",
|
"messageformat": "No results for \"{searchTerm}\"",
|
||||||
"description": "Shown in the search left pane when no results were found"
|
"description": "Shown in the search left pane when no results were found"
|
||||||
},
|
},
|
||||||
|
"icu:noSearchResultsWithUnreadFilter": {
|
||||||
|
"messageformat": "No results for \"{searchTerm}\" in unread chats",
|
||||||
|
"description": "Shown in the search left pane when no results were found with a search query and filter by unread enabled"
|
||||||
|
},
|
||||||
|
"icu:noSearchResultsOnlyUnreadFilter": {
|
||||||
|
"messageformat": "No unread chats",
|
||||||
|
"description": "Shown in the search left pane when no results were found with only filter by unread enabled"
|
||||||
|
},
|
||||||
"icu:noSearchResults--sms-only": {
|
"icu:noSearchResults--sms-only": {
|
||||||
"messageformat": "SMS/MMS contacts are not available on Desktop.",
|
"messageformat": "SMS/MMS contacts are not available on Desktop.",
|
||||||
"description": "Shown in the search left pane when no results were found and primary device has SMS/MMS handling enabled"
|
"description": "Shown in the search left pane when no results were found and primary device has SMS/MMS handling enabled"
|
||||||
|
@ -947,6 +959,18 @@
|
||||||
"messageformat": "No results for \"{searchTerm}\" in {conversationName}",
|
"messageformat": "No results for \"{searchTerm}\" in {conversationName}",
|
||||||
"description": "Shown in the search left pane when no results were found"
|
"description": "Shown in the search left pane when no results were found"
|
||||||
},
|
},
|
||||||
|
"icu:conversationsUnreadHeader": {
|
||||||
|
"messageformat": "Filtered by unread",
|
||||||
|
"description": "Shown to inform the user that the results are based off of filter by unread"
|
||||||
|
},
|
||||||
|
"icu:filterByUnreadButtonLabel": {
|
||||||
|
"messageformat": "Filter by unread",
|
||||||
|
"description": "Shown when you hover over the filter by unread button"
|
||||||
|
},
|
||||||
|
"icu:clearFilterButton": {
|
||||||
|
"messageformat": "Clear filter",
|
||||||
|
"description": "Action button for clearing a filter"
|
||||||
|
},
|
||||||
"icu:conversationsHeader": {
|
"icu:conversationsHeader": {
|
||||||
"messageformat": "Chats",
|
"messageformat": "Chats",
|
||||||
"description": "Shown to separate the types of search results"
|
"description": "Shown to separate the types of search results"
|
||||||
|
@ -7389,10 +7413,18 @@
|
||||||
"messageformat": "Click <newCallButtonIcon></newCallButtonIcon> to start a new voice or video call.",
|
"messageformat": "Click <newCallButtonIcon></newCallButtonIcon> to start a new voice or video call.",
|
||||||
"description": "Calls Tab > When no call is selected > Empty state > Call to action text"
|
"description": "Calls Tab > When no call is selected > Empty state > Call to action text"
|
||||||
},
|
},
|
||||||
|
"icu:CallsList__SearchInputPlaceholder--missed-calls": {
|
||||||
|
"messageformat": "Search missed calls",
|
||||||
|
"description": "Calls Tab > Calls List > Search Input > Enable missed calls filter > Placeholder"
|
||||||
|
},
|
||||||
"icu:CallsList__SearchInputPlaceholder": {
|
"icu:CallsList__SearchInputPlaceholder": {
|
||||||
"messageformat": "Search",
|
"messageformat": "Search",
|
||||||
"description": "Calls Tab > Calls List > Search Input > Placeholder"
|
"description": "Calls Tab > Calls List > Search Input > Placeholder"
|
||||||
},
|
},
|
||||||
|
"icu:CallsList__FilteredByMissedHeader": {
|
||||||
|
"messageformat": "Filtered by missed",
|
||||||
|
"description": "Calls Tab > Calls List > Toggle search filter by missed > Header"
|
||||||
|
},
|
||||||
"icu:CallsList__ToggleFilterByMissedLabel": {
|
"icu:CallsList__ToggleFilterByMissedLabel": {
|
||||||
"messageformat": "Filter by missed",
|
"messageformat": "Filter by missed",
|
||||||
"description": "Calls Tab > Calls List > Toggle search filter by missed > Accessibility label"
|
"description": "Calls Tab > Calls List > Toggle search filter by missed > Accessibility label"
|
||||||
|
@ -7409,17 +7441,17 @@
|
||||||
"messageformat": "Recent calls will appear here.",
|
"messageformat": "Recent calls will appear here.",
|
||||||
"description": "Calls Tab > Calls List > When no results found > With no search query. Message subtitle"
|
"description": "Calls Tab > Calls List > When no results found > With no search query. Message subtitle"
|
||||||
},
|
},
|
||||||
"icu:CallsList__EmptyState--noQuery--missed__title": {
|
|
||||||
"messageformat": "No missed calls",
|
|
||||||
"description": "Calls Tab > Calls List > When no missed calls found > With no search query. Message title"
|
|
||||||
},
|
|
||||||
"icu:CallsList__EmptyState--noQuery--missed__subtitle": {
|
|
||||||
"messageformat": "Missed calls will appear here.",
|
|
||||||
"description": "Calls Tab > Calls List > When no missed calls found > With no search query. Message subtitle"
|
|
||||||
},
|
|
||||||
"icu:CallsList__EmptyState--hasQuery": {
|
"icu:CallsList__EmptyState--hasQuery": {
|
||||||
"messageformat": "No results for “{query}”",
|
"messageformat": "No results for “{query}”",
|
||||||
"description": "Calls Tab > Calls List > When no results found > With a search query"
|
"description": "Calls Tab > Calls List > When no results found > With just a search query"
|
||||||
|
},
|
||||||
|
"icu:CallsList__EmptyState--hasQueryAndMissedCalls": {
|
||||||
|
"messageformat": "No results for “{query}” in missed calls",
|
||||||
|
"description": "Calls Tab > Calls List > When no results found > With a search query and missed call filter enabled"
|
||||||
|
},
|
||||||
|
"icu:CallsList__EmptyState--missedCalls": {
|
||||||
|
"messageformat": "No missed calls",
|
||||||
|
"description": "Calls Tab > Calls List > When no results found > With only missed call filter enabled"
|
||||||
},
|
},
|
||||||
"icu:CallsList__CreateCallLink": {
|
"icu:CallsList__CreateCallLink": {
|
||||||
"messageformat": "Create a Call Link",
|
"messageformat": "Create a Call Link",
|
||||||
|
|
|
@ -5270,6 +5270,10 @@ button.module-calling-participants-list__contact {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--clear-filter-button {
|
||||||
|
height: $normal-row-height;
|
||||||
|
}
|
||||||
|
|
||||||
&--header {
|
&--header {
|
||||||
@include font-body-1-bold;
|
@include font-body-1-bold;
|
||||||
|
|
||||||
|
@ -5520,10 +5524,25 @@ button.module-calling-participants-list__contact {
|
||||||
background-color: $color-gray-75;
|
background-color: $color-gray-75;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.module-left-pane__no-search-results__unread-header {
|
||||||
|
margin-bottom: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-left-pane__no-search-results--withHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 15px;
|
||||||
|
// This applies only for filter by unread, set margin
|
||||||
|
// for clear filter button
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-inline: 1em;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
.module-left-pane__no-search-results,
|
.module-left-pane__no-search-results,
|
||||||
.module-left-pane__compose-no-contacts {
|
.module-left-pane__compose-no-contacts {
|
||||||
flex-grow: 1;
|
|
||||||
margin-top: 27px;
|
margin-top: 27px;
|
||||||
padding-inline: 1em;
|
padding-inline: 1em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -5531,6 +5550,10 @@ button.module-calling-participants-list__contact {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-left-pane__compose-no-contacts {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.module-left-pane__no-search-results__sms-only {
|
.module-left-pane__no-search-results__sms-only {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
|
|
|
@ -129,6 +129,21 @@
|
||||||
gap: 0px;
|
gap: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.CallsList__FilterHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@include font-body-1-bold;
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
padding-inline-start: 24px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.CallsList__ToggleFilterByMissed {
|
.CallsList__ToggleFilterByMissed {
|
||||||
@include button-reset;
|
@include button-reset;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
39
stylesheets/components/ClearFilterButton.scss
Normal file
39
stylesheets/components/ClearFilterButton.scss
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.ClearFilterButton {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
&__inner-vertical-center {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__inner {
|
||||||
|
border-radius: 50px;
|
||||||
|
padding-block: 5px;
|
||||||
|
padding-inline: 15px;
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: mix($color-gray-80, $color-gray-65, 40%);
|
||||||
|
color: $color-white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@include not-disabled {
|
||||||
|
background-color: $color-gray-65;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include light-theme {
|
||||||
|
background-color: mix($color-gray-04, $color-white, 15%);
|
||||||
|
color: $color-black;
|
||||||
|
&:hover {
|
||||||
|
@include not-disabled {
|
||||||
|
background-color: $color-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,4 +60,64 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__FilterButton {
|
||||||
|
@include button-reset;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:not(.LeftPaneSearchInput__FilterButton--pressed):hover {
|
||||||
|
@include light-theme {
|
||||||
|
background-color: $color-black-alpha-06;
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
background-color: $color-white-alpha-06;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
@include keyboard-mode {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px $color-white,
|
||||||
|
0 0 0 4px $color-ultramarine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
@include light-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/filter/filter.svg',
|
||||||
|
$color-black
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@include dark-theme {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/filter/filter.svg',
|
||||||
|
$color-gray-15
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pressed {
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: $color-accent-blue;
|
||||||
|
&::before {
|
||||||
|
@include color-svg(
|
||||||
|
'../images/icons/v3/filter/filter.svg',
|
||||||
|
$color-white
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__FilterLabel {
|
||||||
|
@include sr-only;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
@import './components/ChatColorPicker.scss';
|
@import './components/ChatColorPicker.scss';
|
||||||
@import './components/Checkbox.scss';
|
@import './components/Checkbox.scss';
|
||||||
@import './components/CircleCheckbox.scss';
|
@import './components/CircleCheckbox.scss';
|
||||||
|
@import './components/ClearFilterButton.scss';
|
||||||
@import './components/CollidingAvatars.scss';
|
@import './components/CollidingAvatars.scss';
|
||||||
@import './components/ComposeStepButton.scss';
|
@import './components/ComposeStepButton.scss';
|
||||||
@import './components/CompositionArea.scss';
|
@import './components/CompositionArea.scss';
|
||||||
|
|
|
@ -66,6 +66,8 @@ import type {
|
||||||
} from '../state/ducks/calling';
|
} from '../state/ducks/calling';
|
||||||
import { DAY, MINUTE, SECOND } from '../util/durations';
|
import { DAY, MINUTE, SECOND } from '../util/durations';
|
||||||
import type { StartCallData } from './ConfirmLeaveCallModal';
|
import type { StartCallData } from './ConfirmLeaveCallModal';
|
||||||
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
import type { ICUJSXMessageParamsByKeyType } from '../types/Util';
|
||||||
|
|
||||||
function Timestamp({
|
function Timestamp({
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -153,6 +155,7 @@ type CallsListProps = Readonly<{
|
||||||
togglePip: () => void;
|
togglePip: () => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
const FILTER_HEADER_ROW_HEIGHT = 50;
|
||||||
const CALL_LIST_ITEM_ROW_HEIGHT = 62;
|
const CALL_LIST_ITEM_ROW_HEIGHT = 62;
|
||||||
const INACTIVE_CALL_LINKS_TO_PEEK = 10;
|
const INACTIVE_CALL_LINKS_TO_PEEK = 10;
|
||||||
const INACTIVE_CALL_LINK_AGE_THRESHOLD = 10 * DAY;
|
const INACTIVE_CALL_LINK_AGE_THRESHOLD = 10 * DAY;
|
||||||
|
@ -167,7 +170,11 @@ function isSameOptions(
|
||||||
return a.query === b.query && a.status === b.status;
|
return a.query === b.query && a.status === b.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SpecialRows = 'CreateCallLink' | 'EmptyState';
|
type SpecialRows =
|
||||||
|
| 'CreateCallLink'
|
||||||
|
| 'EmptyState'
|
||||||
|
| 'FilterHeader'
|
||||||
|
| 'ClearFilterButton';
|
||||||
type Row = CallHistoryGroup | SpecialRows;
|
type Row = CallHistoryGroup | SpecialRows;
|
||||||
|
|
||||||
export function CallsList({
|
export function CallsList({
|
||||||
|
@ -206,25 +213,34 @@ export function CallsList({
|
||||||
const searchStateStatus =
|
const searchStateStatus =
|
||||||
searchState.options?.status ?? CallHistoryFilterStatus.All;
|
searchState.options?.status ?? CallHistoryFilterStatus.All;
|
||||||
const hasSearchStateQuery = searchStateQuery !== '';
|
const hasSearchStateQuery = searchStateQuery !== '';
|
||||||
const searchFiltering =
|
const hasMissedCallFilter =
|
||||||
hasSearchStateQuery || searchStateStatus !== CallHistoryFilterStatus.All;
|
searchStateStatus === CallHistoryFilterStatus.Missed;
|
||||||
|
const searchFiltering = hasSearchStateQuery || hasMissedCallFilter;
|
||||||
const searchPending = searchState.state === 'pending';
|
const searchPending = searchState.state === 'pending';
|
||||||
const isEmpty = !searchState.results?.items?.length;
|
const isEmpty = !searchState.results?.items?.length;
|
||||||
|
|
||||||
const rows = useMemo(() => {
|
const rows = useMemo<ReadonlyArray<Row>>(() => {
|
||||||
let results: ReadonlyArray<Row> = searchState.results?.items ?? [];
|
const results: ReadonlyArray<Row> = searchState.results?.items ?? [];
|
||||||
if (results.length === 0 && hasSearchStateQuery) {
|
|
||||||
results = ['EmptyState'];
|
if (results.length === 0 && searchFiltering) {
|
||||||
|
return hasMissedCallFilter
|
||||||
|
? ['FilterHeader', 'EmptyState', 'ClearFilterButton']
|
||||||
|
: ['EmptyState'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!searchFiltering && canCreateCallLinks) {
|
if (!searchFiltering && canCreateCallLinks) {
|
||||||
results = ['CreateCallLink', ...results];
|
return ['CreateCallLink', ...results];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMissedCallFilter) {
|
||||||
|
return ['FilterHeader', ...results, 'ClearFilterButton'];
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
}, [
|
}, [
|
||||||
searchState.results?.items,
|
searchState.results?.items,
|
||||||
hasSearchStateQuery,
|
|
||||||
searchFiltering,
|
searchFiltering,
|
||||||
canCreateCallLinks,
|
canCreateCallLinks,
|
||||||
|
hasMissedCallFilter,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const rowCount = rows.length;
|
const rowCount = rows.length;
|
||||||
|
@ -675,10 +691,8 @@ export function CallsList({
|
||||||
({ index }: Index) => {
|
({ index }: Index) => {
|
||||||
const item = rows.at(index) ?? null;
|
const item = rows.at(index) ?? null;
|
||||||
|
|
||||||
if (item === 'EmptyState') {
|
if (item === 'FilterHeader') {
|
||||||
// arbitary large number so the empty state can be as big as it wants,
|
return FILTER_HEADER_ROW_HEIGHT;
|
||||||
// scrolling should always be locked when the list is empty
|
|
||||||
return 9999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return CALL_LIST_ITEM_ROW_HEIGHT;
|
return CALL_LIST_ITEM_ROW_HEIGHT;
|
||||||
|
@ -710,11 +724,23 @@ export function CallsList({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item === 'EmptyState') {
|
if (item === 'EmptyState') {
|
||||||
|
let i18nId: keyof ICUJSXMessageParamsByKeyType;
|
||||||
|
|
||||||
|
if (hasSearchStateQuery && hasMissedCallFilter) {
|
||||||
|
i18nId = 'icu:CallsList__EmptyState--hasQueryAndMissedCalls';
|
||||||
|
} else if (hasSearchStateQuery) {
|
||||||
|
i18nId = 'icu:CallsList__EmptyState--hasQuery';
|
||||||
|
} else if (hasMissedCallFilter) {
|
||||||
|
i18nId = 'icu:CallsList__EmptyState--missedCalls';
|
||||||
|
} else {
|
||||||
|
// This should never happen
|
||||||
|
i18nId = 'icu:CallsList__EmptyState--hasQuery';
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div key={key} className="CallsList__EmptyState" style={style}>
|
<div key={key} className="CallsList__EmptyState" style={style}>
|
||||||
<I18n
|
<I18n
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id="icu:CallsList__EmptyState--hasQuery"
|
id={i18nId}
|
||||||
components={{
|
components={{
|
||||||
query: <UserText text={searchStateQuery} />,
|
query: <UserText text={searchStateQuery} />,
|
||||||
}}
|
}}
|
||||||
|
@ -723,6 +749,32 @@ export function CallsList({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item === 'FilterHeader') {
|
||||||
|
return (
|
||||||
|
<div key={key} style={style} className="CallsList__FilterHeader">
|
||||||
|
{i18n('icu:CallsList__FilteredByMissedHeader')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item === 'ClearFilterButton') {
|
||||||
|
return (
|
||||||
|
<div key={key} style={style} className="ClearFilterButton">
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.SecondaryAffirmative}
|
||||||
|
className={classNames('ClearFilterButton__inner', {
|
||||||
|
// The clear filter button should be closer to the emty state
|
||||||
|
// text than to the search results.
|
||||||
|
'ClearFilterButton__inner-vertical-center': !isEmpty,
|
||||||
|
})}
|
||||||
|
onClick={() => setStatusInput(CallHistoryFilterStatus.All)}
|
||||||
|
>
|
||||||
|
{i18n('icu:clearFilterButton')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const conversation = getConversationForItem(item);
|
const conversation = getConversationForItem(item);
|
||||||
const activeCallConversationId = activeCall?.conversationId;
|
const activeCallConversationId = activeCall?.conversationId;
|
||||||
|
|
||||||
|
@ -918,6 +970,8 @@ export function CallsList({
|
||||||
getIsAnybodyInCall,
|
getIsAnybodyInCall,
|
||||||
getIsCallActive,
|
getIsCallActive,
|
||||||
getIsInCall,
|
getIsInCall,
|
||||||
|
hasMissedCallFilter,
|
||||||
|
hasSearchStateQuery,
|
||||||
selectedCallHistoryGroup,
|
selectedCallHistoryGroup,
|
||||||
onChangeCallsTabSelectedView,
|
onChangeCallsTabSelectedView,
|
||||||
onCreateCallLink,
|
onCreateCallLink,
|
||||||
|
@ -927,6 +981,7 @@ export function CallsList({
|
||||||
toggleConfirmLeaveCallModal,
|
toggleConfirmLeaveCallModal,
|
||||||
togglePip,
|
togglePip,
|
||||||
i18n,
|
i18n,
|
||||||
|
isEmpty,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -957,20 +1012,14 @@ export function CallsList({
|
||||||
subtitle={i18n('icu:CallsList__EmptyState--noQuery__subtitle')}
|
subtitle={i18n('icu:CallsList__EmptyState--noQuery__subtitle')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isEmpty &&
|
|
||||||
statusInput === CallHistoryFilterStatus.Missed &&
|
|
||||||
!hasSearchStateQuery && (
|
|
||||||
<NavSidebarEmpty
|
|
||||||
title={i18n('icu:CallsList__EmptyState--noQuery--missed__title')}
|
|
||||||
subtitle={i18n(
|
|
||||||
'icu:CallsList__EmptyState--noQuery--missed__subtitle'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<NavSidebarSearchHeader>
|
<NavSidebarSearchHeader>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
placeholder={i18n('icu:CallsList__SearchInputPlaceholder')}
|
placeholder={
|
||||||
|
searchFiltering
|
||||||
|
? i18n('icu:CallsList__SearchInputPlaceholder--missed-calls')
|
||||||
|
: i18n('icu:CallsList__SearchInputPlaceholder')
|
||||||
|
}
|
||||||
onChange={handleSearchInputChange}
|
onChange={handleSearchInputChange}
|
||||||
onClear={handleSearchInputClear}
|
onClear={handleSearchInputClear}
|
||||||
value={queryInput}
|
value={queryInput}
|
||||||
|
@ -983,11 +1032,10 @@ export function CallsList({
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className={classNames('CallsList__ToggleFilterByMissed', {
|
className={classNames('CallsList__ToggleFilterByMissed', {
|
||||||
'CallsList__ToggleFilterByMissed--pressed':
|
'CallsList__ToggleFilterByMissed--pressed': hasMissedCallFilter,
|
||||||
statusInput === CallHistoryFilterStatus.Missed,
|
|
||||||
})}
|
})}
|
||||||
type="button"
|
type="button"
|
||||||
aria-pressed={statusInput === CallHistoryFilterStatus.Missed}
|
aria-pressed={hasMissedCallFilter}
|
||||||
aria-roledescription={i18n(
|
aria-roledescription={i18n(
|
||||||
'icu:CallsList__ToggleFilterByMissed__RoleDescription'
|
'icu:CallsList__ToggleFilterByMissed__RoleDescription'
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -77,6 +77,7 @@ function Wrapper({
|
||||||
'onOutgoingVideoCallInConversation'
|
'onOutgoingVideoCallInConversation'
|
||||||
)}
|
)}
|
||||||
onClickArchiveButton={action('onClickArchiveButton')}
|
onClickArchiveButton={action('onClickArchiveButton')}
|
||||||
|
onClickClearFilterButton={action('onClickClearFilterButton')}
|
||||||
onClickContactCheckbox={action('onClickContactCheckbox')}
|
onClickContactCheckbox={action('onClickContactCheckbox')}
|
||||||
removeConversation={action('removeConversation')}
|
removeConversation={action('removeConversation')}
|
||||||
renderMessageSearchResult={(id: string) => (
|
renderMessageSearchResult={(id: string) => (
|
||||||
|
|
|
@ -36,11 +36,13 @@ import { SearchResultsLoadingFakeRow as SearchResultsLoadingFakeRowComponent } f
|
||||||
import { UsernameSearchResultListItem } from './conversationList/UsernameSearchResultListItem';
|
import { UsernameSearchResultListItem } from './conversationList/UsernameSearchResultListItem';
|
||||||
import { GroupListItem } from './conversationList/GroupListItem';
|
import { GroupListItem } from './conversationList/GroupListItem';
|
||||||
import { ListView } from './ListView';
|
import { ListView } from './ListView';
|
||||||
|
import { Button, ButtonVariant } from './Button';
|
||||||
|
|
||||||
export enum RowType {
|
export enum RowType {
|
||||||
ArchiveButton = 'ArchiveButton',
|
ArchiveButton = 'ArchiveButton',
|
||||||
Blank = 'Blank',
|
Blank = 'Blank',
|
||||||
Contact = 'Contact',
|
Contact = 'Contact',
|
||||||
|
ClearFilterButton = 'ClearFilterButton',
|
||||||
ContactCheckbox = 'ContactCheckbox',
|
ContactCheckbox = 'ContactCheckbox',
|
||||||
PhoneNumberCheckbox = 'PhoneNumberCheckbox',
|
PhoneNumberCheckbox = 'PhoneNumberCheckbox',
|
||||||
UsernameCheckbox = 'UsernameCheckbox',
|
UsernameCheckbox = 'UsernameCheckbox',
|
||||||
|
@ -72,6 +74,11 @@ type ContactRowType = {
|
||||||
hasContextMenu?: boolean;
|
hasContextMenu?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ClearFilterButtonRowType = {
|
||||||
|
type: RowType.ClearFilterButton;
|
||||||
|
isOnNoResultsPage: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type ContactCheckboxRowType = {
|
type ContactCheckboxRowType = {
|
||||||
type: RowType.ContactCheckbox;
|
type: RowType.ContactCheckbox;
|
||||||
contact: ContactListItemPropsType;
|
contact: ContactListItemPropsType;
|
||||||
|
@ -158,6 +165,7 @@ export type Row =
|
||||||
| BlankRowType
|
| BlankRowType
|
||||||
| ContactRowType
|
| ContactRowType
|
||||||
| ContactCheckboxRowType
|
| ContactCheckboxRowType
|
||||||
|
| ClearFilterButtonRowType
|
||||||
| PhoneNumberCheckboxRowType
|
| PhoneNumberCheckboxRowType
|
||||||
| UsernameCheckboxRowType
|
| UsernameCheckboxRowType
|
||||||
| ConversationRowType
|
| ConversationRowType
|
||||||
|
@ -197,6 +205,7 @@ export type PropsType = {
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
disabledReason: undefined | ContactCheckboxDisabledReason
|
disabledReason: undefined | ContactCheckboxDisabledReason
|
||||||
) => void;
|
) => void;
|
||||||
|
onClickClearFilterButton: () => void;
|
||||||
onPreloadConversation: (conversationId: string, messageId?: string) => void;
|
onPreloadConversation: (conversationId: string, messageId?: string) => void;
|
||||||
onSelectConversation: (conversationId: string, messageId?: string) => void;
|
onSelectConversation: (conversationId: string, messageId?: string) => void;
|
||||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||||
|
@ -221,6 +230,7 @@ export function ConversationList({
|
||||||
blockConversation,
|
blockConversation,
|
||||||
onClickArchiveButton,
|
onClickArchiveButton,
|
||||||
onClickContactCheckbox,
|
onClickContactCheckbox,
|
||||||
|
onClickClearFilterButton,
|
||||||
onPreloadConversation,
|
onPreloadConversation,
|
||||||
onSelectConversation,
|
onSelectConversation,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
|
@ -332,6 +342,24 @@ export function ConversationList({
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
case RowType.ClearFilterButton:
|
||||||
|
result = (
|
||||||
|
<div className="ClearFilterButton module-conversation-list__item--clear-filter-button">
|
||||||
|
<Button
|
||||||
|
variant={ButtonVariant.SecondaryAffirmative}
|
||||||
|
className={classNames('ClearFilterButton__inner', {
|
||||||
|
// The clear filter button should be closer to the empty state
|
||||||
|
// text than to the search results.
|
||||||
|
'ClearFilterButton__inner-vertical-center':
|
||||||
|
!row.isOnNoResultsPage,
|
||||||
|
})}
|
||||||
|
onClick={onClickClearFilterButton}
|
||||||
|
>
|
||||||
|
{i18n('icu:clearFilterButton')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
break;
|
||||||
case RowType.PhoneNumberCheckbox:
|
case RowType.PhoneNumberCheckbox:
|
||||||
result = (
|
result = (
|
||||||
<PhoneNumberCheckboxComponent
|
<PhoneNumberCheckboxComponent
|
||||||
|
@ -527,6 +555,7 @@ export function ConversationList({
|
||||||
i18n,
|
i18n,
|
||||||
lookupConversationWithoutServiceId,
|
lookupConversationWithoutServiceId,
|
||||||
onClickArchiveButton,
|
onClickArchiveButton,
|
||||||
|
onClickClearFilterButton,
|
||||||
onClickContactCheckbox,
|
onClickContactCheckbox,
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
onOutgoingVideoCallInConversation,
|
onOutgoingVideoCallInConversation,
|
||||||
|
|
|
@ -400,6 +400,7 @@ export function ForwardMessagesModal({
|
||||||
showConversation={shouldNeverBeCalled}
|
showConversation={shouldNeverBeCalled}
|
||||||
showUserNotFoundModal={shouldNeverBeCalled}
|
showUserNotFoundModal={shouldNeverBeCalled}
|
||||||
setIsFetchingUUID={shouldNeverBeCalled}
|
setIsFetchingUUID={shouldNeverBeCalled}
|
||||||
|
onClickClearFilterButton={shouldNeverBeCalled}
|
||||||
onPreloadConversation={shouldNeverBeCalled}
|
onPreloadConversation={shouldNeverBeCalled}
|
||||||
onSelectConversation={shouldNeverBeCalled}
|
onSelectConversation={shouldNeverBeCalled}
|
||||||
blockConversation={shouldNeverBeCalled}
|
blockConversation={shouldNeverBeCalled}
|
||||||
|
|
|
@ -62,6 +62,7 @@ const defaultConversations: Array<ConversationType> = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const defaultSearchProps = {
|
const defaultSearchProps = {
|
||||||
|
filterByUnread: false,
|
||||||
isSearchingGlobally: true,
|
isSearchingGlobally: true,
|
||||||
searchConversation: undefined,
|
searchConversation: undefined,
|
||||||
searchDisabled: false,
|
searchDisabled: false,
|
||||||
|
@ -110,6 +111,7 @@ const pinnedConversations: Array<ConversationType> = [
|
||||||
|
|
||||||
const defaultModeSpecificProps = {
|
const defaultModeSpecificProps = {
|
||||||
...defaultSearchProps,
|
...defaultSearchProps,
|
||||||
|
filterByUnread: false,
|
||||||
mode: LeftPaneMode.Inbox as const,
|
mode: LeftPaneMode.Inbox as const,
|
||||||
pinnedConversations,
|
pinnedConversations,
|
||||||
conversations: defaultConversations,
|
conversations: defaultConversations,
|
||||||
|
@ -153,7 +155,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||||
},
|
},
|
||||||
clearConversationSearch: action('clearConversationSearch'),
|
clearConversationSearch: action('clearConversationSearch'),
|
||||||
clearGroupCreationError: action('clearGroupCreationError'),
|
clearGroupCreationError: action('clearGroupCreationError'),
|
||||||
clearSearch: action('clearSearch'),
|
clearSearchQuery: action('clearSearchQuery'),
|
||||||
closeMaximumGroupSizeModal: action('closeMaximumGroupSizeModal'),
|
closeMaximumGroupSizeModal: action('closeMaximumGroupSizeModal'),
|
||||||
closeRecommendedGroupSizeModal: action('closeRecommendedGroupSizeModal'),
|
closeRecommendedGroupSizeModal: action('closeRecommendedGroupSizeModal'),
|
||||||
composeDeleteAvatarFromDisk: action('composeDeleteAvatarFromDisk'),
|
composeDeleteAvatarFromDisk: action('composeDeleteAvatarFromDisk'),
|
||||||
|
@ -316,6 +318,7 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||||
),
|
),
|
||||||
toggleNavTabsCollapse: action('toggleNavTabsCollapse'),
|
toggleNavTabsCollapse: action('toggleNavTabsCollapse'),
|
||||||
toggleProfileEditor: action('toggleProfileEditor'),
|
toggleProfileEditor: action('toggleProfileEditor'),
|
||||||
|
updateFilterByUnread: action('updateFilterByUnread'),
|
||||||
updateSearchTerm: action('updateSearchTerm'),
|
updateSearchTerm: action('updateSearchTerm'),
|
||||||
|
|
||||||
...overrideProps,
|
...overrideProps,
|
||||||
|
@ -600,6 +603,43 @@ export function SearchNoResultsWhenSearchingInAConversation(): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SearchNoResultsUnreadFilterAndQuery(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<LeftPaneInContainer
|
||||||
|
{...useProps({
|
||||||
|
modeSpecificProps: {
|
||||||
|
...defaultSearchProps,
|
||||||
|
filterByUnread: true,
|
||||||
|
mode: LeftPaneMode.Search,
|
||||||
|
conversationResults: emptySearchResultsGroup,
|
||||||
|
contactResults: emptySearchResultsGroup,
|
||||||
|
messageResults: emptySearchResultsGroup,
|
||||||
|
primarySendsSms: false,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchNoResultsUnreadFilterWithoutQuery(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<LeftPaneInContainer
|
||||||
|
{...useProps({
|
||||||
|
modeSpecificProps: {
|
||||||
|
...defaultSearchProps,
|
||||||
|
searchTerm: '',
|
||||||
|
filterByUnread: true,
|
||||||
|
mode: LeftPaneMode.Search,
|
||||||
|
conversationResults: emptySearchResultsGroup,
|
||||||
|
contactResults: emptySearchResultsGroup,
|
||||||
|
messageResults: emptySearchResultsGroup,
|
||||||
|
primarySendsSms: false,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SearchAllResultsLoading(): JSX.Element {
|
export function SearchAllResultsLoading(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<LeftPaneInContainer
|
<LeftPaneInContainer
|
||||||
|
@ -683,6 +723,30 @@ export function SearchAllResults(): JSX.Element {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SearchAllResultsUnreadFilter(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<LeftPaneInContainer
|
||||||
|
{...useProps({
|
||||||
|
modeSpecificProps: {
|
||||||
|
...defaultSearchProps,
|
||||||
|
filterByUnread: true,
|
||||||
|
mode: LeftPaneMode.Search,
|
||||||
|
conversationResults: {
|
||||||
|
isLoading: false,
|
||||||
|
results: defaultConversations,
|
||||||
|
},
|
||||||
|
contactResults: { isLoading: false, results: [] },
|
||||||
|
messageResults: {
|
||||||
|
isLoading: false,
|
||||||
|
results: [],
|
||||||
|
},
|
||||||
|
primarySendsSms: false,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ArchiveNoArchivedConversations(): JSX.Element {
|
export function ArchiveNoArchivedConversations(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<LeftPaneInContainer
|
<LeftPaneInContainer
|
||||||
|
|
|
@ -121,7 +121,7 @@ export type PropsType = {
|
||||||
blockConversation: (conversationId: string) => void;
|
blockConversation: (conversationId: string) => void;
|
||||||
clearConversationSearch: () => void;
|
clearConversationSearch: () => void;
|
||||||
clearGroupCreationError: () => void;
|
clearGroupCreationError: () => void;
|
||||||
clearSearch: () => void;
|
clearSearchQuery: () => void;
|
||||||
closeMaximumGroupSizeModal: () => void;
|
closeMaximumGroupSizeModal: () => void;
|
||||||
closeRecommendedGroupSizeModal: () => void;
|
closeRecommendedGroupSizeModal: () => void;
|
||||||
composeDeleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
composeDeleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||||
|
@ -160,7 +160,8 @@ export type PropsType = {
|
||||||
toggleConversationInChooseMembers: (conversationId: string) => void;
|
toggleConversationInChooseMembers: (conversationId: string) => void;
|
||||||
toggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
toggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
|
||||||
toggleProfileEditor: (initialEditState?: ProfileEditorEditState) => void;
|
toggleProfileEditor: (initialEditState?: ProfileEditorEditState) => void;
|
||||||
updateSearchTerm: (_: string) => void;
|
updateSearchTerm: (query: string) => void;
|
||||||
|
updateFilterByUnread: (filterByUnread: boolean) => void;
|
||||||
|
|
||||||
// Render Props
|
// Render Props
|
||||||
renderMessageSearchResult: (id: string) => JSX.Element;
|
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||||
|
@ -192,7 +193,7 @@ export function LeftPane({
|
||||||
challengeStatus,
|
challengeStatus,
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearGroupCreationError,
|
clearGroupCreationError,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
closeMaximumGroupSizeModal,
|
closeMaximumGroupSizeModal,
|
||||||
closeRecommendedGroupSizeModal,
|
closeRecommendedGroupSizeModal,
|
||||||
composeDeleteAvatarFromDisk,
|
composeDeleteAvatarFromDisk,
|
||||||
|
@ -264,6 +265,7 @@ export function LeftPane({
|
||||||
usernameLinkCorrupted,
|
usernameLinkCorrupted,
|
||||||
updateSearchTerm,
|
updateSearchTerm,
|
||||||
dismissBackupMediaDownloadBanner,
|
dismissBackupMediaDownloadBanner,
|
||||||
|
updateFilterByUnread,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
const previousModeSpecificProps = usePrevious(
|
const previousModeSpecificProps = usePrevious(
|
||||||
modeSpecificProps,
|
modeSpecificProps,
|
||||||
|
@ -460,7 +462,7 @@ export function LeftPane({
|
||||||
const { conversationId, messageId } = conversationToOpen;
|
const { conversationId, messageId } = conversationToOpen;
|
||||||
showConversation({ conversationId, messageId });
|
showConversation({ conversationId, messageId });
|
||||||
if (openedByNumber) {
|
if (openedByNumber) {
|
||||||
clearSearch();
|
clearSearchQuery();
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -478,7 +480,7 @@ export function LeftPane({
|
||||||
document.removeEventListener('keydown', onKeyDown);
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
helper,
|
helper,
|
||||||
isMacOS,
|
isMacOS,
|
||||||
searchInConversation,
|
searchInConversation,
|
||||||
|
@ -498,7 +500,7 @@ export function LeftPane({
|
||||||
const preRowsNode = helper.getPreRowsNode({
|
const preRowsNode = helper.getPreRowsNode({
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearGroupCreationError,
|
clearGroupCreationError,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
closeMaximumGroupSizeModal,
|
closeMaximumGroupSizeModal,
|
||||||
closeRecommendedGroupSizeModal,
|
closeRecommendedGroupSizeModal,
|
||||||
composeDeleteAvatarFromDisk,
|
composeDeleteAvatarFromDisk,
|
||||||
|
@ -758,7 +760,7 @@ export function LeftPane({
|
||||||
<NavSidebarSearchHeader>
|
<NavSidebarSearchHeader>
|
||||||
{helper.getSearchInput({
|
{helper.getSearchInput({
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
endConversationSearch,
|
endConversationSearch,
|
||||||
endSearch,
|
endSearch,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -772,6 +774,7 @@ export function LeftPane({
|
||||||
showUserNotFoundModal,
|
showUserNotFoundModal,
|
||||||
setIsFetchingUUID,
|
setIsFetchingUUID,
|
||||||
showInbox,
|
showInbox,
|
||||||
|
updateFilterByUnread,
|
||||||
})}
|
})}
|
||||||
</NavSidebarSearchHeader>
|
</NavSidebarSearchHeader>
|
||||||
)}
|
)}
|
||||||
|
@ -826,6 +829,9 @@ export function LeftPane({
|
||||||
throw missingCaseError(disabledReason);
|
throw missingCaseError(disabledReason);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onClickClearFilterButton={() => {
|
||||||
|
updateFilterByUnread(false);
|
||||||
|
}}
|
||||||
showUserNotFoundModal={showUserNotFoundModal}
|
showUserNotFoundModal={showUserNotFoundModal}
|
||||||
setIsFetchingUUID={setIsFetchingUUID}
|
setIsFetchingUUID={setIsFetchingUUID}
|
||||||
lookupConversationWithoutServiceId={
|
lookupConversationWithoutServiceId={
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import type {
|
import type {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
ShowConversationType,
|
ShowConversationType,
|
||||||
|
@ -10,17 +11,19 @@ import type { LocalizerType } from '../types/Util';
|
||||||
import { Avatar, AvatarSize } from './Avatar';
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { usePrevious } from '../hooks/usePrevious';
|
import { usePrevious } from '../hooks/usePrevious';
|
||||||
|
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||||
|
import { Theme } from '../util/theme';
|
||||||
|
|
||||||
type PropsType = {
|
type BasePropsType = {
|
||||||
clearConversationSearch: () => void;
|
clearConversationSearch: () => void;
|
||||||
clearSearch: () => void;
|
clearSearchQuery: () => void;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
endConversationSearch: () => void;
|
endConversationSearch: () => void;
|
||||||
endSearch: () => void;
|
endSearch: () => void;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
isSearchingGlobally: boolean;
|
isSearchingGlobally: boolean;
|
||||||
onEnterKeyDown?: (
|
onEnterKeyDown?: (
|
||||||
clearSearch: () => void,
|
clearSearchQuery: () => void,
|
||||||
showConversation: ShowConversationType
|
showConversation: ShowConversationType
|
||||||
) => void;
|
) => void;
|
||||||
searchConversation?: ConversationType;
|
searchConversation?: ConversationType;
|
||||||
|
@ -30,9 +33,23 @@ type PropsType = {
|
||||||
updateSearchTerm: (searchTerm: string) => void;
|
updateSearchTerm: (searchTerm: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type NoFilterPropsType = BasePropsType & {
|
||||||
|
filterButtonEnabled?: false;
|
||||||
|
filterPressed?: false;
|
||||||
|
onFilterClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WithFilterPropsType = BasePropsType & {
|
||||||
|
filterButtonEnabled: boolean;
|
||||||
|
filterPressed: boolean;
|
||||||
|
onFilterClick: (enabled: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PropsType = NoFilterPropsType | WithFilterPropsType;
|
||||||
|
|
||||||
export function LeftPaneSearchInput({
|
export function LeftPaneSearchInput({
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
disabled,
|
disabled,
|
||||||
endConversationSearch,
|
endConversationSearch,
|
||||||
endSearch,
|
endSearch,
|
||||||
|
@ -44,6 +61,9 @@ export function LeftPaneSearchInput({
|
||||||
showConversation,
|
showConversation,
|
||||||
startSearchCounter,
|
startSearchCounter,
|
||||||
updateSearchTerm,
|
updateSearchTerm,
|
||||||
|
filterButtonEnabled = false,
|
||||||
|
filterPressed = false,
|
||||||
|
onFilterClick,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
const inputRef = useRef<null | HTMLInputElement>(null);
|
const inputRef = useRef<null | HTMLInputElement>(null);
|
||||||
|
|
||||||
|
@ -83,7 +103,7 @@ export function LeftPaneSearchInput({
|
||||||
if (searchConversation) {
|
if (searchConversation) {
|
||||||
clearConversationSearch();
|
clearConversationSearch();
|
||||||
} else {
|
} else {
|
||||||
clearSearch();
|
clearSearchQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -94,79 +114,109 @@ export function LeftPaneSearchInput({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const label = searchConversation ? i18n('icu:searchIn') : i18n('icu:search');
|
let label: string;
|
||||||
|
if (searchConversation) {
|
||||||
|
label = i18n('icu:searchIn');
|
||||||
|
} else if (filterPressed) {
|
||||||
|
label = i18n('icu:searchUnreadChats');
|
||||||
|
} else {
|
||||||
|
label = i18n('icu:search');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchInput
|
<>
|
||||||
disabled={disabled}
|
<SearchInput
|
||||||
label={label}
|
disabled={disabled}
|
||||||
hasSearchIcon={!searchConversation}
|
label={label}
|
||||||
i18n={i18n}
|
hasSearchIcon={!searchConversation}
|
||||||
moduleClassName="LeftPaneSearchInput"
|
i18n={i18n}
|
||||||
onBlur={() => {
|
moduleClassName="LeftPaneSearchInput"
|
||||||
if (!searchConversation && !searchTerm) {
|
onBlur={() => {
|
||||||
endSearch();
|
if (!searchConversation && !searchTerm) {
|
||||||
}
|
endSearch();
|
||||||
}}
|
}
|
||||||
onKeyDown={event => {
|
}}
|
||||||
if (onEnterKeyDown && event.key === 'Enter') {
|
onKeyDown={event => {
|
||||||
onEnterKeyDown(clearSearch, showConversation);
|
if (onEnterKeyDown && event.key === 'Enter') {
|
||||||
event.preventDefault();
|
onEnterKeyDown(clearSearchQuery, showConversation);
|
||||||
event.stopPropagation();
|
event.preventDefault();
|
||||||
}
|
event.stopPropagation();
|
||||||
}}
|
}
|
||||||
onChange={event => {
|
}}
|
||||||
changeValue(event.currentTarget.value);
|
onChange={event => {
|
||||||
}}
|
changeValue(event.currentTarget.value);
|
||||||
onClear={() => {
|
}}
|
||||||
if (searchTerm) {
|
onClear={() => {
|
||||||
clearSearch();
|
if (searchTerm) {
|
||||||
inputRef.current?.focus();
|
clearSearchQuery();
|
||||||
} else if (searchConversation) {
|
|
||||||
endConversationSearch();
|
|
||||||
inputRef.current?.focus();
|
|
||||||
} else {
|
|
||||||
inputRef.current?.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ref={inputRef}
|
|
||||||
placeholder={label}
|
|
||||||
value={searchTerm}
|
|
||||||
>
|
|
||||||
{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();
|
inputRef.current?.focus();
|
||||||
}}
|
} else if (searchConversation) {
|
||||||
|
endConversationSearch();
|
||||||
|
inputRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={inputRef}
|
||||||
|
placeholder={label}
|
||||||
|
value={searchTerm}
|
||||||
|
>
|
||||||
|
{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}
|
||||||
|
avatarUrl={searchConversation.avatarUrl}
|
||||||
|
badge={undefined}
|
||||||
|
color={searchConversation.color}
|
||||||
|
conversationType={searchConversation.type}
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={searchConversation.isMe}
|
||||||
|
noteToSelf={searchConversation.isMe}
|
||||||
|
sharedGroupNames={searchConversation.sharedGroupNames}
|
||||||
|
size={AvatarSize.TWENTY}
|
||||||
|
title={searchConversation.title}
|
||||||
|
unblurredAvatarUrl={searchConversation.unblurredAvatarUrl}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
aria-label={i18n('icu:clearSearch')}
|
||||||
|
className="LeftPaneSearchInput__in-conversation-pill__x-button"
|
||||||
|
onClick={endConversationSearch}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SearchInput>
|
||||||
|
{filterButtonEnabled && (
|
||||||
|
<Tooltip
|
||||||
|
direction={TooltipPlacement.Bottom}
|
||||||
|
content={i18n('icu:filterByUnreadButtonLabel')}
|
||||||
|
theme={Theme.Dark}
|
||||||
|
delay={600}
|
||||||
>
|
>
|
||||||
<Avatar
|
|
||||||
acceptedMessageRequest={searchConversation.acceptedMessageRequest}
|
|
||||||
avatarUrl={searchConversation.avatarUrl}
|
|
||||||
badge={undefined}
|
|
||||||
color={searchConversation.color}
|
|
||||||
conversationType={searchConversation.type}
|
|
||||||
i18n={i18n}
|
|
||||||
isMe={searchConversation.isMe}
|
|
||||||
noteToSelf={searchConversation.isMe}
|
|
||||||
sharedGroupNames={searchConversation.sharedGroupNames}
|
|
||||||
size={AvatarSize.TWENTY}
|
|
||||||
title={searchConversation.title}
|
|
||||||
unblurredAvatarUrl={searchConversation.unblurredAvatarUrl}
|
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
aria-label={i18n('icu:clearSearch')}
|
className={classNames('LeftPaneSearchInput__FilterButton', {
|
||||||
className="LeftPaneSearchInput__in-conversation-pill__x-button"
|
'LeftPaneSearchInput__FilterButton--pressed': filterPressed,
|
||||||
onClick={endConversationSearch}
|
})}
|
||||||
type="button"
|
type="button"
|
||||||
/>
|
aria-pressed={filterPressed}
|
||||||
</div>
|
onClick={() => onFilterClick?.(!filterPressed)}
|
||||||
|
>
|
||||||
|
<span className="LeftPaneSearchInput__FilterLabel">
|
||||||
|
{i18n('icu:filterByUnreadButtonLabel')}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</SearchInput>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1217,6 +1217,7 @@ export function EditDistributionListModal({
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
lookupConversationWithoutServiceId={asyncShouldNeverBeCalled}
|
lookupConversationWithoutServiceId={asyncShouldNeverBeCalled}
|
||||||
onClickArchiveButton={shouldNeverBeCalled}
|
onClickArchiveButton={shouldNeverBeCalled}
|
||||||
|
onClickClearFilterButton={shouldNeverBeCalled}
|
||||||
onClickContactCheckbox={(conversationId: string) => {
|
onClickContactCheckbox={(conversationId: string) => {
|
||||||
toggleSelectedConversation(conversationId);
|
toggleSelectedConversation(conversationId);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -85,7 +85,7 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
|
|
||||||
override getSearchInput({
|
override getSearchInput({
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
endConversationSearch,
|
endConversationSearch,
|
||||||
endSearch,
|
endSearch,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -93,7 +93,7 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
showConversation,
|
showConversation,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
clearConversationSearch: () => unknown;
|
clearConversationSearch: () => unknown;
|
||||||
clearSearch: () => unknown;
|
clearSearchQuery: () => unknown;
|
||||||
endConversationSearch: () => unknown;
|
endConversationSearch: () => unknown;
|
||||||
endSearch: () => unknown;
|
endSearch: () => unknown;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -107,7 +107,7 @@ export class LeftPaneArchiveHelper extends LeftPaneHelper<LeftPaneArchivePropsTy
|
||||||
return (
|
return (
|
||||||
<LeftPaneSearchInput
|
<LeftPaneSearchInput
|
||||||
clearConversationSearch={clearConversationSearch}
|
clearConversationSearch={clearConversationSearch}
|
||||||
clearSearch={clearSearch}
|
clearSearchQuery={clearSearchQuery}
|
||||||
endConversationSearch={endConversationSearch}
|
endConversationSearch={endConversationSearch}
|
||||||
endSearch={endSearch}
|
endSearch={endSearch}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -39,7 +39,7 @@ export abstract class LeftPaneHelper<T> {
|
||||||
getSearchInput(
|
getSearchInput(
|
||||||
_: Readonly<{
|
_: Readonly<{
|
||||||
clearConversationSearch: () => unknown;
|
clearConversationSearch: () => unknown;
|
||||||
clearSearch: () => unknown;
|
clearSearchQuery: () => unknown;
|
||||||
endConversationSearch: () => unknown;
|
endConversationSearch: () => unknown;
|
||||||
endSearch: () => unknown;
|
endSearch: () => unknown;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -50,6 +50,7 @@ export abstract class LeftPaneHelper<T> {
|
||||||
updateSearchTerm: (searchTerm: string) => unknown;
|
updateSearchTerm: (searchTerm: string) => unknown;
|
||||||
showConversation: ShowConversationType;
|
showConversation: ShowConversationType;
|
||||||
showInbox: () => void;
|
showInbox: () => void;
|
||||||
|
updateFilterByUnread: (filterByUnread: boolean) => void;
|
||||||
}> &
|
}> &
|
||||||
LookupConversationWithoutServiceIdActionsType
|
LookupConversationWithoutServiceIdActionsType
|
||||||
): null | ReactChild {
|
): null | ReactChild {
|
||||||
|
@ -78,7 +79,7 @@ export abstract class LeftPaneHelper<T> {
|
||||||
_: Readonly<{
|
_: Readonly<{
|
||||||
clearConversationSearch: () => unknown;
|
clearConversationSearch: () => unknown;
|
||||||
clearGroupCreationError: () => void;
|
clearGroupCreationError: () => void;
|
||||||
clearSearch: () => unknown;
|
clearSearchQuery: () => unknown;
|
||||||
closeMaximumGroupSizeModal: () => unknown;
|
closeMaximumGroupSizeModal: () => unknown;
|
||||||
closeRecommendedGroupSizeModal: () => unknown;
|
closeRecommendedGroupSizeModal: () => unknown;
|
||||||
composeDeleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
composeDeleteAvatarFromDisk: DeleteAvatarFromDiskActionType;
|
||||||
|
|
|
@ -30,6 +30,7 @@ export type LeftPaneInboxPropsType = {
|
||||||
searchDisabled: boolean;
|
searchDisabled: boolean;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
searchConversation: undefined | ConversationType;
|
searchConversation: undefined | ConversationType;
|
||||||
|
filterByUnread: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType> {
|
export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType> {
|
||||||
|
@ -51,6 +52,8 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
||||||
|
|
||||||
private readonly searchConversation: undefined | ConversationType;
|
private readonly searchConversation: undefined | ConversationType;
|
||||||
|
|
||||||
|
private readonly filterByUnread: boolean;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
conversations,
|
conversations,
|
||||||
archivedConversations,
|
archivedConversations,
|
||||||
|
@ -61,6 +64,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
||||||
searchDisabled,
|
searchDisabled,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
searchConversation,
|
searchConversation,
|
||||||
|
filterByUnread,
|
||||||
}: Readonly<LeftPaneInboxPropsType>) {
|
}: Readonly<LeftPaneInboxPropsType>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -73,6 +77,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
||||||
this.searchDisabled = searchDisabled;
|
this.searchDisabled = searchDisabled;
|
||||||
this.searchTerm = searchTerm;
|
this.searchTerm = searchTerm;
|
||||||
this.searchConversation = searchConversation;
|
this.searchConversation = searchConversation;
|
||||||
|
this.filterByUnread = filterByUnread;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowCount(): number {
|
getRowCount(): number {
|
||||||
|
@ -88,25 +93,27 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
||||||
|
|
||||||
override getSearchInput({
|
override getSearchInput({
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
endConversationSearch,
|
endConversationSearch,
|
||||||
endSearch,
|
endSearch,
|
||||||
i18n,
|
i18n,
|
||||||
showConversation,
|
showConversation,
|
||||||
updateSearchTerm,
|
updateSearchTerm,
|
||||||
|
updateFilterByUnread,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
clearConversationSearch: () => unknown;
|
clearConversationSearch: () => unknown;
|
||||||
clearSearch: () => unknown;
|
clearSearchQuery: () => unknown;
|
||||||
endConversationSearch: () => unknown;
|
endConversationSearch: () => unknown;
|
||||||
endSearch: () => unknown;
|
endSearch: () => unknown;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
showConversation: ShowConversationType;
|
showConversation: ShowConversationType;
|
||||||
updateSearchTerm: (searchTerm: string) => unknown;
|
updateSearchTerm: (searchTerm: string) => unknown;
|
||||||
|
updateFilterByUnread: (filterByUnread: boolean) => void;
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
return (
|
return (
|
||||||
<LeftPaneSearchInput
|
<LeftPaneSearchInput
|
||||||
clearConversationSearch={clearConversationSearch}
|
clearConversationSearch={clearConversationSearch}
|
||||||
clearSearch={clearSearch}
|
clearSearchQuery={clearSearchQuery}
|
||||||
endConversationSearch={endConversationSearch}
|
endConversationSearch={endConversationSearch}
|
||||||
endSearch={endSearch}
|
endSearch={endSearch}
|
||||||
disabled={this.searchDisabled}
|
disabled={this.searchDisabled}
|
||||||
|
@ -117,6 +124,9 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
|
||||||
showConversation={showConversation}
|
showConversation={showConversation}
|
||||||
startSearchCounter={this.startSearchCounter}
|
startSearchCounter={this.startSearchCounter}
|
||||||
updateSearchTerm={updateSearchTerm}
|
updateSearchTerm={updateSearchTerm}
|
||||||
|
onFilterClick={updateFilterByUnread}
|
||||||
|
filterButtonEnabled={!this.searchConversation}
|
||||||
|
filterPressed={this.filterByUnread}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ export type LeftPaneSearchPropsType = {
|
||||||
searchConversationName?: string;
|
searchConversationName?: string;
|
||||||
primarySendsSms: boolean;
|
primarySendsSms: boolean;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
filterByUnread: boolean;
|
||||||
startSearchCounter: number;
|
startSearchCounter: number;
|
||||||
isSearchingGlobally: boolean;
|
isSearchingGlobally: boolean;
|
||||||
searchDisabled: boolean;
|
searchDisabled: boolean;
|
||||||
|
@ -78,6 +79,8 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
|
|
||||||
private readonly searchConversation: undefined | ConversationType;
|
private readonly searchConversation: undefined | ConversationType;
|
||||||
|
|
||||||
|
private readonly filterByUnread: boolean;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
contactResults,
|
contactResults,
|
||||||
conversationResults,
|
conversationResults,
|
||||||
|
@ -89,6 +92,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
searchDisabled,
|
searchDisabled,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
startSearchCounter,
|
startSearchCounter,
|
||||||
|
filterByUnread,
|
||||||
}: Readonly<LeftPaneSearchPropsType>) {
|
}: Readonly<LeftPaneSearchPropsType>) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -102,30 +106,33 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
this.searchDisabled = searchDisabled;
|
this.searchDisabled = searchDisabled;
|
||||||
this.searchTerm = searchTerm;
|
this.searchTerm = searchTerm;
|
||||||
this.startSearchCounter = startSearchCounter;
|
this.startSearchCounter = startSearchCounter;
|
||||||
|
this.filterByUnread = filterByUnread;
|
||||||
this.onEnterKeyDown = this.onEnterKeyDown.bind(this);
|
this.onEnterKeyDown = this.onEnterKeyDown.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
override getSearchInput({
|
override getSearchInput({
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
endConversationSearch,
|
endConversationSearch,
|
||||||
endSearch,
|
endSearch,
|
||||||
i18n,
|
i18n,
|
||||||
showConversation,
|
showConversation,
|
||||||
updateSearchTerm,
|
updateSearchTerm,
|
||||||
|
updateFilterByUnread,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
clearConversationSearch: () => unknown;
|
clearConversationSearch: () => unknown;
|
||||||
clearSearch: () => unknown;
|
clearSearchQuery: () => unknown;
|
||||||
endConversationSearch: () => unknown;
|
endConversationSearch: () => unknown;
|
||||||
endSearch: () => unknown;
|
endSearch: () => unknown;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
showConversation: ShowConversationType;
|
showConversation: ShowConversationType;
|
||||||
updateSearchTerm: (searchTerm: string) => unknown;
|
updateSearchTerm: (searchTerm: string) => unknown;
|
||||||
|
updateFilterByUnread: (filterByUnread: boolean) => void;
|
||||||
}>): ReactChild {
|
}>): ReactChild {
|
||||||
return (
|
return (
|
||||||
<LeftPaneSearchInput
|
<LeftPaneSearchInput
|
||||||
clearConversationSearch={clearConversationSearch}
|
clearConversationSearch={clearConversationSearch}
|
||||||
clearSearch={clearSearch}
|
clearSearchQuery={clearSearchQuery}
|
||||||
endConversationSearch={endConversationSearch}
|
endConversationSearch={endConversationSearch}
|
||||||
endSearch={endSearch}
|
endSearch={endSearch}
|
||||||
disabled={this.searchDisabled}
|
disabled={this.searchDisabled}
|
||||||
|
@ -137,6 +144,9 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
showConversation={showConversation}
|
showConversation={showConversation}
|
||||||
startSearchCounter={this.startSearchCounter}
|
startSearchCounter={this.startSearchCounter}
|
||||||
updateSearchTerm={updateSearchTerm}
|
updateSearchTerm={updateSearchTerm}
|
||||||
|
filterButtonEnabled={!this.searchConversation}
|
||||||
|
filterPressed={this.filterByUnread}
|
||||||
|
onFilterClick={updateFilterByUnread}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -171,13 +181,29 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
let noResultsMessage: string;
|
||||||
|
if (this.filterByUnread && this.searchTerm.length > 0) {
|
||||||
|
noResultsMessage = i18n('icu:noSearchResultsWithUnreadFilter', {
|
||||||
|
searchTerm,
|
||||||
|
});
|
||||||
|
} else if (this.filterByUnread) {
|
||||||
|
noResultsMessage = i18n('icu:noSearchResultsOnlyUnreadFilter');
|
||||||
|
} else {
|
||||||
|
noResultsMessage = i18n('icu:noSearchResults', {
|
||||||
|
searchTerm,
|
||||||
|
});
|
||||||
|
}
|
||||||
noResults = (
|
noResults = (
|
||||||
<>
|
<>
|
||||||
<div>
|
{this.filterByUnread && (
|
||||||
{i18n('icu:noSearchResults', {
|
<div
|
||||||
searchTerm,
|
className="module-conversation-list__item--header module-left-pane__no-search-results__unread-header"
|
||||||
})}
|
aria-label={i18n('icu:conversationsUnreadHeader')}
|
||||||
</div>
|
>
|
||||||
|
{i18n('icu:conversationsUnreadHeader')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>{noResultsMessage}</div>
|
||||||
{primarySendsSms && (
|
{primarySendsSms && (
|
||||||
<div className="module-left-pane__no-search-results__sms-only">
|
<div className="module-left-pane__no-search-results__sms-only">
|
||||||
{i18n('icu:noSearchResults--sms-only')}
|
{i18n('icu:noSearchResults--sms-only')}
|
||||||
|
@ -191,7 +217,11 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
<div
|
<div
|
||||||
// We need this for Ctrl-T shortcut cycling through parts of app
|
// We need this for Ctrl-T shortcut cycling through parts of app
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="module-left-pane__no-search-results"
|
className={
|
||||||
|
this.filterByUnread
|
||||||
|
? 'module-left-pane__no-search-results--withHeader'
|
||||||
|
: 'module-left-pane__no-search-results'
|
||||||
|
}
|
||||||
key={searchTerm}
|
key={searchTerm}
|
||||||
>
|
>
|
||||||
{noResults}
|
{noResults}
|
||||||
|
@ -205,11 +235,18 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
return 1 + SEARCH_RESULTS_FAKE_ROW_COUNT;
|
return 1 + SEARCH_RESULTS_FAKE_ROW_COUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.allResults().reduce(
|
let count = this.allResults().reduce(
|
||||||
(result: number, searchResults) =>
|
(result: number, searchResults) =>
|
||||||
result + getRowCountForLoadedSearchResults(searchResults),
|
result + getRowCountForLoadedSearchResults(searchResults),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The clear unread filter button adds an extra row
|
||||||
|
if (this.filterByUnread) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is currently unimplemented. See DESKTOP-1170.
|
// This is currently unimplemented. See DESKTOP-1170.
|
||||||
|
@ -236,12 +273,19 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
getRowCountForLoadedSearchResults(conversationResults);
|
getRowCountForLoadedSearchResults(conversationResults);
|
||||||
const contactRowCount = getRowCountForLoadedSearchResults(contactResults);
|
const contactRowCount = getRowCountForLoadedSearchResults(contactResults);
|
||||||
const messageRowCount = getRowCountForLoadedSearchResults(messageResults);
|
const messageRowCount = getRowCountForLoadedSearchResults(messageResults);
|
||||||
|
const clearFilterButtonRowCount = this.filterByUnread ? 1 : 0;
|
||||||
|
|
||||||
if (rowIndex < conversationRowCount) {
|
let rowOffset = 0;
|
||||||
|
|
||||||
|
rowOffset += conversationRowCount;
|
||||||
|
if (rowIndex < rowOffset) {
|
||||||
if (rowIndex === 0) {
|
if (rowIndex === 0) {
|
||||||
return {
|
return {
|
||||||
type: RowType.Header,
|
type: RowType.Header,
|
||||||
getHeaderText: i18n => i18n('icu:conversationsHeader'),
|
getHeaderText: i18n =>
|
||||||
|
this.filterByUnread
|
||||||
|
? i18n('icu:conversationsUnreadHeader')
|
||||||
|
: i18n('icu:conversationsHeader'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
assertDev(
|
assertDev(
|
||||||
|
@ -257,7 +301,9 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rowIndex < conversationRowCount + contactRowCount) {
|
rowOffset += contactRowCount;
|
||||||
|
|
||||||
|
if (rowIndex < rowOffset) {
|
||||||
const localIndex = rowIndex - conversationRowCount;
|
const localIndex = rowIndex - conversationRowCount;
|
||||||
if (localIndex === 0) {
|
if (localIndex === 0) {
|
||||||
return {
|
return {
|
||||||
|
@ -278,28 +324,40 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rowIndex >= conversationRowCount + contactRowCount + messageRowCount) {
|
rowOffset += messageRowCount;
|
||||||
return undefined;
|
if (rowIndex < rowOffset) {
|
||||||
|
const localIndex = rowIndex - conversationRowCount - contactRowCount;
|
||||||
|
if (localIndex === 0) {
|
||||||
|
return {
|
||||||
|
type: RowType.Header,
|
||||||
|
getHeaderText: i18n => i18n('icu:messagesHeader'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
assertDev(
|
||||||
|
!messageResults.isLoading,
|
||||||
|
"We shouldn't get here with message results still loading"
|
||||||
|
);
|
||||||
|
const message = messageResults.results[localIndex - 1];
|
||||||
|
return message
|
||||||
|
? {
|
||||||
|
type: RowType.MessageSearchResult,
|
||||||
|
messageId: message.id,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const localIndex = rowIndex - conversationRowCount - contactRowCount;
|
rowOffset += clearFilterButtonRowCount;
|
||||||
if (localIndex === 0) {
|
if (rowIndex < rowOffset) {
|
||||||
return {
|
return {
|
||||||
type: RowType.Header,
|
type: RowType.ClearFilterButton,
|
||||||
getHeaderText: i18n => i18n('icu:messagesHeader'),
|
isOnNoResultsPage: this.allResults().every(
|
||||||
|
searchResult =>
|
||||||
|
searchResult.isLoading || searchResult.results.length === 0
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
assertDev(
|
|
||||||
!messageResults.isLoading,
|
return undefined;
|
||||||
"We shouldn't get here with message results still loading"
|
|
||||||
);
|
|
||||||
const message = messageResults.results[localIndex - 1];
|
|
||||||
return message
|
|
||||||
? {
|
|
||||||
type: RowType.MessageSearchResult,
|
|
||||||
messageId: message.id,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override isScrollable(): boolean {
|
override isScrollable(): boolean {
|
||||||
|
@ -307,7 +365,8 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRecomputeRowHeights(old: Readonly<LeftPaneSearchPropsType>): boolean {
|
shouldRecomputeRowHeights(old: Readonly<LeftPaneSearchPropsType>): boolean {
|
||||||
const oldIsLoading = new LeftPaneSearchHelper(old).isLoading();
|
const oldSearchPaneHelper = new LeftPaneSearchHelper(old);
|
||||||
|
const oldIsLoading = oldSearchPaneHelper.isLoading();
|
||||||
const newIsLoading = this.isLoading();
|
const newIsLoading = this.isLoading();
|
||||||
if (oldIsLoading && newIsLoading) {
|
if (oldIsLoading && newIsLoading) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -376,7 +435,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
}
|
}
|
||||||
|
|
||||||
private onEnterKeyDown(
|
private onEnterKeyDown(
|
||||||
clearSearch: () => unknown,
|
clearSearchQuery: () => unknown,
|
||||||
showConversation: ShowConversationType
|
showConversation: ShowConversationType
|
||||||
): void {
|
): void {
|
||||||
const conversation = this.getConversationAndMessageAtIndex(0);
|
const conversation = this.getConversationAndMessageAtIndex(0);
|
||||||
|
@ -384,7 +443,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showConversation(conversation);
|
showConversation(conversation);
|
||||||
clearSearch();
|
clearSearchQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,7 @@ import {
|
||||||
import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
|
import { MAX_MESSAGE_COUNT } from '../../util/deleteForMe.types';
|
||||||
import { markCallHistoryReadInConversation } from './callHistory';
|
import { markCallHistoryReadInConversation } from './callHistory';
|
||||||
import type { CapabilitiesType } from '../../textsecure/WebAPI';
|
import type { CapabilitiesType } from '../../textsecure/WebAPI';
|
||||||
|
import { actions as searchActions } from './search';
|
||||||
import type { SearchActionType } from './search';
|
import type { SearchActionType } from './search';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
@ -2683,18 +2684,33 @@ function setPreJoinConversation(
|
||||||
function conversationsUpdated(
|
function conversationsUpdated(
|
||||||
data: Array<ConversationType>
|
data: Array<ConversationType>
|
||||||
): ThunkAction<void, RootStateType, unknown, ConversationsUpdatedActionType> {
|
): ThunkAction<void, RootStateType, unknown, ConversationsUpdatedActionType> {
|
||||||
return dispatch => {
|
return (dispatch, getState) => {
|
||||||
for (const conversation of data) {
|
for (const conversation of data) {
|
||||||
calling.groupMembersChanged(conversation.id);
|
calling.groupMembersChanged(conversation.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { conversationLookup } = getState().conversations;
|
||||||
|
|
||||||
|
const someConversationsHaveNewMessages = data.some(conversation => {
|
||||||
|
return (
|
||||||
|
conversationLookup[conversation.id]?.lastMessageReceivedAt !==
|
||||||
|
conversation.lastMessageReceivedAt
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'CONVERSATIONS_UPDATED',
|
type: 'CONVERSATIONS_UPDATED',
|
||||||
payload: {
|
payload: {
|
||||||
data,
|
data,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (someConversationsHaveNewMessages) {
|
||||||
|
dispatch(searchActions.refreshSearch());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function conversationRemoved(id: string): ConversationRemovedActionType {
|
function conversationRemoved(id: string): ConversationRemovedActionType {
|
||||||
return {
|
return {
|
||||||
type: 'CONVERSATION_REMOVED',
|
type: 'CONVERSATION_REMOVED',
|
||||||
|
|
|
@ -24,7 +24,12 @@ import type {
|
||||||
ShowArchivedConversationsActionType,
|
ShowArchivedConversationsActionType,
|
||||||
MessageType,
|
MessageType,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import { getQuery, getSearchConversation } from '../selectors/search';
|
import {
|
||||||
|
getFilterByUnread,
|
||||||
|
getIsActivelySearching,
|
||||||
|
getQuery,
|
||||||
|
getSearchConversation,
|
||||||
|
} from '../selectors/search';
|
||||||
import { getAllConversations } from '../selectors/conversations';
|
import { getAllConversations } from '../selectors/conversations';
|
||||||
import {
|
import {
|
||||||
getIntl,
|
getIntl,
|
||||||
|
@ -62,6 +67,7 @@ export type SearchStateType = ReadonlyDeep<{
|
||||||
contactIds: Array<string>;
|
contactIds: Array<string>;
|
||||||
conversationIds: Array<string>;
|
conversationIds: Array<string>;
|
||||||
query: string;
|
query: string;
|
||||||
|
filterByUnread: boolean;
|
||||||
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;
|
||||||
|
@ -98,8 +104,8 @@ type StartSearchActionType = ReadonlyDeep<{
|
||||||
type: 'SEARCH_START';
|
type: 'SEARCH_START';
|
||||||
payload: null;
|
payload: null;
|
||||||
}>;
|
}>;
|
||||||
type ClearSearchActionType = ReadonlyDeep<{
|
type ClearSearchQueryActionType = ReadonlyDeep<{
|
||||||
type: 'SEARCH_CLEAR';
|
type: 'SEARCH_QUERY_CLEAR';
|
||||||
payload: null;
|
payload: null;
|
||||||
}>;
|
}>;
|
||||||
type ClearConversationSearchActionType = ReadonlyDeep<{
|
type ClearConversationSearchActionType = ReadonlyDeep<{
|
||||||
|
@ -119,12 +125,22 @@ type SearchInConversationActionType = ReadonlyDeep<{
|
||||||
payload: { searchConversationId: string };
|
payload: { searchConversationId: string };
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type UpdateFilterByUnreadActionType = ReadonlyDeep<{
|
||||||
|
type: 'FILTER_BY_UNREAD_UPDATE';
|
||||||
|
payload: { enabled: boolean };
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type RefreshSearchActionType = ReadonlyDeep<{
|
||||||
|
type: 'SEARCH_REFRESH';
|
||||||
|
payload: null;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type SearchActionType = ReadonlyDeep<
|
export type SearchActionType = ReadonlyDeep<
|
||||||
| SearchMessagesResultsFulfilledActionType
|
| SearchMessagesResultsFulfilledActionType
|
||||||
| SearchDiscussionsResultsFulfilledActionType
|
| SearchDiscussionsResultsFulfilledActionType
|
||||||
| UpdateSearchTermActionType
|
| UpdateSearchTermActionType
|
||||||
| StartSearchActionType
|
| StartSearchActionType
|
||||||
| ClearSearchActionType
|
| ClearSearchQueryActionType
|
||||||
| ClearConversationSearchActionType
|
| ClearConversationSearchActionType
|
||||||
| EndSearchActionType
|
| EndSearchActionType
|
||||||
| EndConversationSearchActionType
|
| EndConversationSearchActionType
|
||||||
|
@ -134,18 +150,22 @@ export type SearchActionType = ReadonlyDeep<
|
||||||
| TargetedConversationChangedActionType
|
| TargetedConversationChangedActionType
|
||||||
| ShowArchivedConversationsActionType
|
| ShowArchivedConversationsActionType
|
||||||
| ConversationUnloadedActionType
|
| ConversationUnloadedActionType
|
||||||
|
| UpdateFilterByUnreadActionType
|
||||||
|
| RefreshSearchActionType
|
||||||
>;
|
>;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
startSearch,
|
startSearch,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
endSearch,
|
endSearch,
|
||||||
endConversationSearch,
|
endConversationSearch,
|
||||||
searchInConversation,
|
searchInConversation,
|
||||||
updateSearchTerm,
|
updateSearchTerm,
|
||||||
|
updateFilterByUnread,
|
||||||
|
refreshSearch,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSearchActions = (): BoundActionCreatorsMapObject<
|
export const useSearchActions = (): BoundActionCreatorsMapObject<
|
||||||
|
@ -158,10 +178,22 @@ function startSearch(): StartSearchActionType {
|
||||||
payload: null,
|
payload: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function clearSearch(): ClearSearchActionType {
|
function clearSearchQuery(): ThunkAction<
|
||||||
return {
|
void,
|
||||||
type: 'SEARCH_CLEAR',
|
RootStateType,
|
||||||
payload: null,
|
unknown,
|
||||||
|
ClearSearchQueryActionType
|
||||||
|
> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'SEARCH_QUERY_CLEAR',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
doSearch({
|
||||||
|
dispatch,
|
||||||
|
state: getState(),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
function clearConversationSearch(): ClearConversationSearchActionType {
|
function clearConversationSearch(): ClearConversationSearchActionType {
|
||||||
|
@ -191,6 +223,49 @@ function searchInConversation(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshSearch(): ThunkAction<
|
||||||
|
void,
|
||||||
|
RootStateType,
|
||||||
|
unknown,
|
||||||
|
RefreshSearchActionType
|
||||||
|
> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
if (!getIsActivelySearching(state)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'SEARCH_REFRESH',
|
||||||
|
payload: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
doSearch({
|
||||||
|
dispatch,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterByUnread(
|
||||||
|
filterByUnread: boolean
|
||||||
|
): ThunkAction<void, RootStateType, unknown, UpdateFilterByUnreadActionType> {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'FILTER_BY_UNREAD_UPDATE',
|
||||||
|
payload: {
|
||||||
|
enabled: filterByUnread,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
doSearch({
|
||||||
|
dispatch,
|
||||||
|
state: getState(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function updateSearchTerm(
|
function updateSearchTerm(
|
||||||
query: string
|
query: string
|
||||||
): ThunkAction<void, RootStateType, unknown, UpdateSearchTermActionType> {
|
): ThunkAction<void, RootStateType, unknown, UpdateSearchTermActionType> {
|
||||||
|
@ -200,23 +275,9 @@ function updateSearchTerm(
|
||||||
payload: { query },
|
payload: { query },
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = getState();
|
|
||||||
const ourConversationId = getUserConversationId(state);
|
|
||||||
strictAssert(
|
|
||||||
ourConversationId,
|
|
||||||
'updateSearchTerm our conversation is missing'
|
|
||||||
);
|
|
||||||
|
|
||||||
const i18n = getIntl(state);
|
|
||||||
|
|
||||||
doSearch({
|
doSearch({
|
||||||
dispatch,
|
dispatch,
|
||||||
allConversations: getAllConversations(state),
|
state: getState(),
|
||||||
regionCode: getRegionCode(state),
|
|
||||||
noteToSelf: i18n('icu:noteToSelf').toLowerCase(),
|
|
||||||
ourConversationId,
|
|
||||||
query: getQuery(state),
|
|
||||||
searchConversationId: getSearchConversation(state)?.id,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -224,12 +285,7 @@ function updateSearchTerm(
|
||||||
const doSearch = debounce(
|
const doSearch = debounce(
|
||||||
({
|
({
|
||||||
dispatch,
|
dispatch,
|
||||||
allConversations,
|
state,
|
||||||
regionCode,
|
|
||||||
noteToSelf,
|
|
||||||
ourConversationId,
|
|
||||||
query,
|
|
||||||
searchConversationId,
|
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
dispatch: ThunkDispatch<
|
dispatch: ThunkDispatch<
|
||||||
RootStateType,
|
RootStateType,
|
||||||
|
@ -237,21 +293,37 @@ const doSearch = debounce(
|
||||||
| SearchMessagesResultsFulfilledActionType
|
| SearchMessagesResultsFulfilledActionType
|
||||||
| SearchDiscussionsResultsFulfilledActionType
|
| SearchDiscussionsResultsFulfilledActionType
|
||||||
>;
|
>;
|
||||||
allConversations: ReadonlyArray<ConversationType>;
|
state: RootStateType;
|
||||||
noteToSelf: string;
|
|
||||||
regionCode: string | undefined;
|
|
||||||
ourConversationId: string;
|
|
||||||
query: string;
|
|
||||||
searchConversationId: undefined | string;
|
|
||||||
}>) => {
|
}>) => {
|
||||||
if (!query) {
|
if (!getIsActivelySearching(state)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const query = getQuery(state);
|
||||||
|
const filterByUnread = getFilterByUnread(state);
|
||||||
|
const i18n = getIntl(state);
|
||||||
|
const allConversations = getAllConversations(state);
|
||||||
|
const regionCode = getRegionCode(state);
|
||||||
|
const noteToSelf = i18n('icu:noteToSelf').toLowerCase();
|
||||||
|
const ourConversationId = getUserConversationId(state);
|
||||||
|
const searchConversationId = getSearchConversation(state)?.id;
|
||||||
|
|
||||||
|
strictAssert(ourConversationId, 'doSearch our conversation is missing');
|
||||||
|
|
||||||
// Limit the number of contacts to something reasonable
|
// Limit the number of contacts to something reasonable
|
||||||
const MAX_MATCHING_CONTACTS = 100;
|
const MAX_MATCHING_CONTACTS = 100;
|
||||||
|
|
||||||
void (async () => {
|
void (async () => {
|
||||||
|
if (filterByUnread) {
|
||||||
|
dispatch({
|
||||||
|
type: 'SEARCH_MESSAGES_RESULTS_FULFILLED',
|
||||||
|
payload: {
|
||||||
|
messages: [],
|
||||||
|
query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const segmenter = new Intl.Segmenter([], { granularity: 'word' });
|
const segmenter = new Intl.Segmenter([], { granularity: 'word' });
|
||||||
const queryWords = [...segmenter.segment(query)]
|
const queryWords = [...segmenter.segment(query)]
|
||||||
.filter(word => word.isWordLike)
|
.filter(word => word.isWordLike)
|
||||||
|
@ -284,6 +356,7 @@ const doSearch = debounce(
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const { conversationIds, contactIds } =
|
const { conversationIds, contactIds } =
|
||||||
await queryConversationsAndContacts(query, {
|
await queryConversationsAndContacts(query, {
|
||||||
|
filterByUnread,
|
||||||
ourConversationId,
|
ourConversationId,
|
||||||
noteToSelf,
|
noteToSelf,
|
||||||
regionCode,
|
regionCode,
|
||||||
|
@ -314,7 +387,7 @@ async function queryMessages({
|
||||||
contactServiceIdsMatchingQuery?: Array<ServiceIdString>;
|
contactServiceIdsMatchingQuery?: Array<ServiceIdString>;
|
||||||
}): Promise<Array<ClientSearchResultMessageType>> {
|
}): Promise<Array<ClientSearchResultMessageType>> {
|
||||||
try {
|
try {
|
||||||
if (query.length === 0) {
|
if (query.trim().length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,6 +411,7 @@ async function queryMessages({
|
||||||
async function queryConversationsAndContacts(
|
async function queryConversationsAndContacts(
|
||||||
query: string,
|
query: string,
|
||||||
options: {
|
options: {
|
||||||
|
filterByUnread: boolean;
|
||||||
ourConversationId: string;
|
ourConversationId: string;
|
||||||
noteToSelf: string;
|
noteToSelf: string;
|
||||||
regionCode: string | undefined;
|
regionCode: string | undefined;
|
||||||
|
@ -347,8 +421,13 @@ async function queryConversationsAndContacts(
|
||||||
contactIds: Array<string>;
|
contactIds: Array<string>;
|
||||||
conversationIds: Array<string>;
|
conversationIds: Array<string>;
|
||||||
}> {
|
}> {
|
||||||
const { ourConversationId, noteToSelf, regionCode, allConversations } =
|
const {
|
||||||
options;
|
filterByUnread,
|
||||||
|
ourConversationId,
|
||||||
|
noteToSelf,
|
||||||
|
regionCode,
|
||||||
|
allConversations,
|
||||||
|
} = options;
|
||||||
|
|
||||||
const normalizedQuery = removeDiacritics(query);
|
const normalizedQuery = removeDiacritics(query);
|
||||||
|
|
||||||
|
@ -382,7 +461,8 @@ async function queryConversationsAndContacts(
|
||||||
const searchResults: Array<ConversationType> = filterAndSortConversations(
|
const searchResults: Array<ConversationType> = filterAndSortConversations(
|
||||||
visibleConversations,
|
visibleConversations,
|
||||||
normalizedQuery,
|
normalizedQuery,
|
||||||
regionCode
|
regionCode,
|
||||||
|
filterByUnread
|
||||||
);
|
);
|
||||||
|
|
||||||
// Split into two groups - active conversations and items just from address book
|
// Split into two groups - active conversations and items just from address book
|
||||||
|
@ -408,6 +488,11 @@ async function queryConversationsAndContacts(
|
||||||
contactIds.unshift(ourConversationId);
|
contactIds.unshift(ourConversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't show contacts in the left pane if we're filtering by unread
|
||||||
|
if (filterByUnread) {
|
||||||
|
contactIds = [];
|
||||||
|
}
|
||||||
|
|
||||||
return { conversationIds, contactIds };
|
return { conversationIds, contactIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -417,6 +502,7 @@ export function getEmptyState(): SearchStateType {
|
||||||
return {
|
return {
|
||||||
startSearchCounter: 0,
|
startSearchCounter: 0,
|
||||||
query: '',
|
query: '',
|
||||||
|
filterByUnread: false,
|
||||||
messageIds: [],
|
messageIds: [],
|
||||||
messageLookup: {},
|
messageLookup: {},
|
||||||
conversationIds: [],
|
conversationIds: [],
|
||||||
|
@ -426,10 +512,51 @@ export function getEmptyState(): SearchStateType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSearchUpdate(
|
||||||
|
state: SearchStateType,
|
||||||
|
params: { query?: string; filterByUnread?: boolean }
|
||||||
|
): SearchStateType {
|
||||||
|
const { query, filterByUnread } = params;
|
||||||
|
|
||||||
|
// Determine the new state values, falling back to existing state if not provided
|
||||||
|
const newQuery = query ?? state.query;
|
||||||
|
const newFilterByUnread = filterByUnread ?? state.filterByUnread;
|
||||||
|
|
||||||
|
const isValidSearch = newQuery.length > 0 || newFilterByUnread;
|
||||||
|
const isWithinConversation = Boolean(state.searchConversationId);
|
||||||
|
|
||||||
|
if (isValidSearch) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
query: newQuery,
|
||||||
|
filterByUnread: newFilterByUnread,
|
||||||
|
messagesLoading: true,
|
||||||
|
messageIds: [],
|
||||||
|
messageLookup: {},
|
||||||
|
discussionsLoading: !isWithinConversation,
|
||||||
|
contactIds: [],
|
||||||
|
conversationIds: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...getEmptyState(),
|
||||||
|
startSearchCounter: state.startSearchCounter,
|
||||||
|
searchConversationId: state.searchConversationId,
|
||||||
|
globalSearch: state.globalSearch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function reducer(
|
export function reducer(
|
||||||
state: Readonly<SearchStateType> = getEmptyState(),
|
state: Readonly<SearchStateType> = getEmptyState(),
|
||||||
action: Readonly<SearchActionType>
|
action: Readonly<SearchActionType>
|
||||||
): SearchStateType {
|
): SearchStateType {
|
||||||
|
if (action.type === 'FILTER_BY_UNREAD_UPDATE') {
|
||||||
|
return handleSearchUpdate(state, {
|
||||||
|
filterByUnread: action.payload.enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
|
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
|
||||||
log.info('search: show archived conversations, clearing message lookup');
|
log.info('search: show archived conversations, clearing message lookup');
|
||||||
return getEmptyState();
|
return getEmptyState();
|
||||||
|
@ -444,15 +571,8 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'SEARCH_CLEAR') {
|
if (action.type === 'SEARCH_QUERY_CLEAR') {
|
||||||
log.info('search: cleared, clearing message lookup');
|
return handleSearchUpdate(state, { query: '' });
|
||||||
|
|
||||||
return {
|
|
||||||
...getEmptyState(),
|
|
||||||
startSearchCounter: state.startSearchCounter,
|
|
||||||
searchConversationId: state.searchConversationId,
|
|
||||||
globalSearch: state.globalSearch,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'SEARCH_END') {
|
if (action.type === 'SEARCH_END') {
|
||||||
|
@ -463,26 +583,7 @@ export function reducer(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'SEARCH_UPDATE') {
|
if (action.type === 'SEARCH_UPDATE') {
|
||||||
const { payload } = action;
|
return handleSearchUpdate(state, { query: action.payload.query });
|
||||||
const { query } = payload;
|
|
||||||
|
|
||||||
const hasQuery = Boolean(query);
|
|
||||||
const isWithinConversation = Boolean(state.searchConversationId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
query,
|
|
||||||
messagesLoading: hasQuery,
|
|
||||||
...(hasQuery
|
|
||||||
? {
|
|
||||||
messageIds: [],
|
|
||||||
messageLookup: {},
|
|
||||||
discussionsLoading: !isWithinConversation,
|
|
||||||
contactIds: [],
|
|
||||||
conversationIds: [],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action.type === 'SEARCH_IN_CONVERSATION') {
|
if (action.type === 'SEARCH_IN_CONVERSATION') {
|
||||||
|
|
|
@ -774,7 +774,9 @@ export const getFilteredCandidateContactsForNewGroup = createSelector(
|
||||||
getCandidateContactsForNewGroup,
|
getCandidateContactsForNewGroup,
|
||||||
getNormalizedComposerConversationSearchTerm,
|
getNormalizedComposerConversationSearchTerm,
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
filterAndSortConversations
|
(contacts, searchTerm, regionCode): Array<ConversationType> => {
|
||||||
|
return filterAndSortConversations(contacts, searchTerm, regionCode);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const getGroupCreationComposerState = createSelector(
|
const getGroupCreationComposerState = createSelector(
|
||||||
|
|
|
@ -34,6 +34,11 @@ import { getOwn } from '../../util/getOwn';
|
||||||
|
|
||||||
export const getSearch = (state: StateType): SearchStateType => state.search;
|
export const getSearch = (state: StateType): SearchStateType => state.search;
|
||||||
|
|
||||||
|
export const getFilterByUnread = createSelector(
|
||||||
|
getSearch,
|
||||||
|
(state: SearchStateType): boolean => state.filterByUnread
|
||||||
|
);
|
||||||
|
|
||||||
export const getQuery = createSelector(
|
export const getQuery = createSelector(
|
||||||
getSearch,
|
getSearch,
|
||||||
(state: SearchStateType): string => state.query
|
(state: SearchStateType): string => state.query
|
||||||
|
@ -96,6 +101,12 @@ export const getHasSearchQuery = createSelector(
|
||||||
(query: string): boolean => query.trim().length > 0
|
(query: string): boolean => query.trim().length > 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getIsActivelySearching = createSelector(
|
||||||
|
[getFilterByUnread, getHasSearchQuery],
|
||||||
|
(filterByUnread: boolean, hasSearchQuery: boolean): boolean =>
|
||||||
|
filterByUnread || hasSearchQuery
|
||||||
|
);
|
||||||
|
|
||||||
export const getMessageSearchResultLookup = createSelector(
|
export const getMessageSearchResultLookup = createSelector(
|
||||||
getSearch,
|
getSearch,
|
||||||
(state: SearchStateType) => state.messageLookup
|
(state: SearchStateType) => state.messageLookup
|
||||||
|
@ -114,6 +125,7 @@ export const getSearchResults = createSelector(
|
||||||
| 'messageResults'
|
| 'messageResults'
|
||||||
| 'searchConversationName'
|
| 'searchConversationName'
|
||||||
| 'searchTerm'
|
| 'searchTerm'
|
||||||
|
| 'filterByUnread'
|
||||||
> => {
|
> => {
|
||||||
const {
|
const {
|
||||||
contactIds,
|
contactIds,
|
||||||
|
@ -145,6 +157,7 @@ export const getSearchResults = createSelector(
|
||||||
},
|
},
|
||||||
searchConversationName,
|
searchConversationName,
|
||||||
searchTerm: state.query,
|
searchTerm: state.query,
|
||||||
|
filterByUnread: state.filterByUnread,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -69,7 +69,9 @@ import {
|
||||||
hasNetworkDialog as getHasNetworkDialog,
|
hasNetworkDialog as getHasNetworkDialog,
|
||||||
} from '../selectors/network';
|
} from '../selectors/network';
|
||||||
import {
|
import {
|
||||||
|
getFilterByUnread,
|
||||||
getHasSearchQuery,
|
getHasSearchQuery,
|
||||||
|
getIsActivelySearching,
|
||||||
getIsSearching,
|
getIsSearching,
|
||||||
getIsSearchingGlobally,
|
getIsSearchingGlobally,
|
||||||
getQuery,
|
getQuery,
|
||||||
|
@ -172,7 +174,7 @@ const getModeSpecificProps = (
|
||||||
...(searchConversation && searchTerm ? getSearchResults(state) : {}),
|
...(searchConversation && searchTerm ? getSearchResults(state) : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (getHasSearchQuery(state)) {
|
if (getIsActivelySearching(state)) {
|
||||||
const primarySendsSms = Boolean(
|
const primarySendsSms = Boolean(
|
||||||
get(state.items, ['primarySendsSms'], false)
|
get(state.items, ['primarySendsSms'], false)
|
||||||
);
|
);
|
||||||
|
@ -195,6 +197,7 @@ const getModeSpecificProps = (
|
||||||
searchDisabled: state.network.challengeStatus !== 'idle',
|
searchDisabled: state.network.challengeStatus !== 'idle',
|
||||||
searchTerm: getQuery(state),
|
searchTerm: getQuery(state),
|
||||||
startSearchCounter: getStartSearchCounter(state),
|
startSearchCounter: getStartSearchCounter(state),
|
||||||
|
filterByUnread: getFilterByUnread(state),
|
||||||
...getLeftPaneLists(state),
|
...getLeftPaneLists(state),
|
||||||
};
|
};
|
||||||
case ComposerStep.StartDirectConversation:
|
case ComposerStep.StartDirectConversation:
|
||||||
|
@ -329,12 +332,13 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
||||||
} = useConversationsActions();
|
} = useConversationsActions();
|
||||||
const {
|
const {
|
||||||
clearConversationSearch,
|
clearConversationSearch,
|
||||||
clearSearch,
|
clearSearchQuery,
|
||||||
endConversationSearch,
|
endConversationSearch,
|
||||||
endSearch,
|
endSearch,
|
||||||
searchInConversation,
|
searchInConversation,
|
||||||
startSearch,
|
startSearch,
|
||||||
updateSearchTerm,
|
updateSearchTerm,
|
||||||
|
updateFilterByUnread,
|
||||||
} = useSearchActions();
|
} = useSearchActions();
|
||||||
const {
|
const {
|
||||||
onOutgoingAudioCallInConversation,
|
onOutgoingAudioCallInConversation,
|
||||||
|
@ -376,7 +380,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
||||||
challengeStatus={challengeStatus}
|
challengeStatus={challengeStatus}
|
||||||
clearConversationSearch={clearConversationSearch}
|
clearConversationSearch={clearConversationSearch}
|
||||||
clearGroupCreationError={clearGroupCreationError}
|
clearGroupCreationError={clearGroupCreationError}
|
||||||
clearSearch={clearSearch}
|
clearSearchQuery={clearSearchQuery}
|
||||||
closeMaximumGroupSizeModal={closeMaximumGroupSizeModal}
|
closeMaximumGroupSizeModal={closeMaximumGroupSizeModal}
|
||||||
closeRecommendedGroupSizeModal={closeRecommendedGroupSizeModal}
|
closeRecommendedGroupSizeModal={closeRecommendedGroupSizeModal}
|
||||||
composeDeleteAvatarFromDisk={composeDeleteAvatarFromDisk}
|
composeDeleteAvatarFromDisk={composeDeleteAvatarFromDisk}
|
||||||
|
@ -448,6 +452,7 @@ export const SmartLeftPane = memo(function SmartLeftPane({
|
||||||
updateSearchTerm={updateSearchTerm}
|
updateSearchTerm={updateSearchTerm}
|
||||||
usernameCorrupted={usernameCorrupted}
|
usernameCorrupted={usernameCorrupted}
|
||||||
usernameLinkCorrupted={usernameLinkCorrupted}
|
usernameLinkCorrupted={usernameLinkCorrupted}
|
||||||
|
updateFilterByUnread={updateFilterByUnread}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -395,6 +395,7 @@ describe('both/state/selectors/search', () => {
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
searchConversationName: undefined,
|
searchConversationName: undefined,
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
filterByUnread: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -450,6 +451,7 @@ describe('both/state/selectors/search', () => {
|
||||||
},
|
},
|
||||||
searchConversationName: undefined,
|
searchConversationName: undefined,
|
||||||
searchTerm: 'foo bar',
|
searchTerm: 'foo bar',
|
||||||
|
filterByUnread: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,6 +14,7 @@ describe('LeftPaneInboxHelper', () => {
|
||||||
const defaultProps: LeftPaneInboxPropsType = {
|
const defaultProps: LeftPaneInboxPropsType = {
|
||||||
archivedConversations: [],
|
archivedConversations: [],
|
||||||
conversations: [],
|
conversations: [],
|
||||||
|
filterByUnread: false,
|
||||||
isSearchingGlobally: false,
|
isSearchingGlobally: false,
|
||||||
isAboutToSearch: false,
|
isAboutToSearch: false,
|
||||||
pinnedConversations: [],
|
pinnedConversations: [],
|
||||||
|
|
|
@ -9,6 +9,18 @@ import { getDefaultConversation } from '../../../test-both/helpers/getDefaultCon
|
||||||
|
|
||||||
import { LeftPaneSearchHelper } from '../../../components/leftPane/LeftPaneSearchHelper';
|
import { LeftPaneSearchHelper } from '../../../components/leftPane/LeftPaneSearchHelper';
|
||||||
|
|
||||||
|
const baseSearchHelperArgs = {
|
||||||
|
conversationResults: { isLoading: false, results: [] },
|
||||||
|
contactResults: { isLoading: false, results: [] },
|
||||||
|
filterByUnread: false,
|
||||||
|
messageResults: { isLoading: false, results: [] },
|
||||||
|
isSearchingGlobally: true,
|
||||||
|
searchTerm: 'foo',
|
||||||
|
primarySendsSms: false,
|
||||||
|
searchConversation: undefined,
|
||||||
|
searchDisabled: false,
|
||||||
|
startSearchCounter: 0,
|
||||||
|
};
|
||||||
describe('LeftPaneSearchHelper', () => {
|
describe('LeftPaneSearchHelper', () => {
|
||||||
const fakeMessage = () => ({
|
const fakeMessage = () => ({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
|
@ -18,17 +30,7 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
|
|
||||||
describe('getBackAction', () => {
|
describe('getBackAction', () => {
|
||||||
it('returns undefined; going back is handled elsewhere in the app', () => {
|
it('returns undefined; going back is handled elsewhere in the app', () => {
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper(baseSearchHelperArgs);
|
||||||
conversationResults: { isLoading: false, results: [] },
|
|
||||||
contactResults: { isLoading: false, results: [] },
|
|
||||||
messageResults: { isLoading: false, results: [] },
|
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.isUndefined(
|
assert.isUndefined(
|
||||||
helper.getBackAction({
|
helper.getBackAction({
|
||||||
|
@ -44,46 +46,31 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
it('returns 100 if any results are loading', () => {
|
it('returns 100 if any results are loading', () => {
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
100
|
100
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
100
|
100
|
||||||
);
|
);
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}).getRowCount(),
|
}).getRowCount(),
|
||||||
100
|
100
|
||||||
);
|
);
|
||||||
|
@ -100,6 +87,7 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
searchConversation: undefined,
|
searchConversation: undefined,
|
||||||
searchDisabled: false,
|
searchDisabled: false,
|
||||||
startSearchCounter: 0,
|
startSearchCounter: 0,
|
||||||
|
filterByUnread: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(helper.getRowCount(), 0);
|
assert.strictEqual(helper.getRowCount(), 0);
|
||||||
|
@ -107,18 +95,13 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
|
|
||||||
it('returns 1 + the number of results, dropping empty sections', () => {
|
it('returns 1 + the number of results, dropping empty sections', () => {
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
contactResults: { isLoading: false, results: [] },
|
||||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.strictEqual(helper.getRowCount(), 5);
|
assert.strictEqual(helper.getRowCount(), 5);
|
||||||
|
@ -129,40 +112,25 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
it('returns a "loading search results" row if any results are loading', () => {
|
it('returns a "loading search results" row if any results are loading', () => {
|
||||||
const helpers = [
|
const helpers = [
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}),
|
}),
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}),
|
}),
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -188,18 +156,13 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
const messages = [fakeMessage(), fakeMessage()];
|
const messages = [fakeMessage(), fakeMessage()];
|
||||||
|
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: conversations,
|
results: conversations,
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: contacts },
|
contactResults: { isLoading: false, results: contacts },
|
||||||
messageResults: { isLoading: false, results: messages },
|
messageResults: { isLoading: false, results: messages },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
|
@ -235,18 +198,9 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
const messages = [fakeMessage(), fakeMessage()];
|
const messages = [fakeMessage(), fakeMessage()];
|
||||||
|
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
conversationResults: {
|
...baseSearchHelperArgs,
|
||||||
isLoading: false,
|
|
||||||
results: [],
|
|
||||||
},
|
|
||||||
contactResults: { isLoading: false, results: contacts },
|
contactResults: { isLoading: false, results: contacts },
|
||||||
messageResults: { isLoading: false, results: messages },
|
messageResults: { isLoading: false, results: messages },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(_testHeaderText(helper.getRow(0)), 'icu:contactsHeader');
|
assert.deepEqual(_testHeaderText(helper.getRow(0)), 'icu:contactsHeader');
|
||||||
|
@ -273,18 +227,12 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
const messages = [fakeMessage(), fakeMessage()];
|
const messages = [fakeMessage(), fakeMessage()];
|
||||||
|
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: conversations,
|
results: conversations,
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
|
||||||
messageResults: { isLoading: false, results: messages },
|
messageResults: { isLoading: false, results: messages },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
|
@ -316,18 +264,12 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
const contacts = [getDefaultConversation()];
|
const contacts = [getDefaultConversation()];
|
||||||
|
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: conversations,
|
results: conversations,
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: contacts },
|
contactResults: { isLoading: false, results: contacts },
|
||||||
messageResults: { isLoading: false, results: [] },
|
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
|
@ -354,40 +296,25 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
it('returns false if any results are loading', () => {
|
it('returns false if any results are loading', () => {
|
||||||
const helpers = [
|
const helpers = [
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}),
|
}),
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}),
|
}),
|
||||||
new LeftPaneSearchHelper({
|
new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -398,21 +325,15 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
|
|
||||||
it('returns true if all results have loaded', () => {
|
it('returns true if all results have loaded', () => {
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
|
||||||
messageResults: {
|
messageResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
||||||
},
|
},
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
assert.isTrue(helper.isScrollable());
|
assert.isTrue(helper.isScrollable());
|
||||||
});
|
});
|
||||||
|
@ -421,25 +342,20 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
describe('shouldRecomputeRowHeights', () => {
|
describe('shouldRecomputeRowHeights', () => {
|
||||||
it("returns false if the number of results doesn't change", () => {
|
it("returns false if the number of results doesn't change", () => {
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
|
||||||
messageResults: {
|
messageResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
||||||
},
|
},
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
helper.shouldRecomputeRowHeights({
|
helper.shouldRecomputeRowHeights({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
|
@ -449,108 +365,68 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
||||||
},
|
},
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'bar',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false when a section completes loading, but not all sections are done (because the pane is still loading overall)', () => {
|
it('returns false when a section completes loading, but not all sections are done (because the pane is still loading overall)', () => {
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isFalse(
|
assert.isFalse(
|
||||||
helper.shouldRecomputeRowHeights({
|
helper.shouldRecomputeRowHeights({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation()],
|
results: [getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: true },
|
messageResults: { isLoading: true },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'bar',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true when all sections finish loading', () => {
|
it('returns true when all sections finish loading', () => {
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
helper.shouldRecomputeRowHeights({
|
helper.shouldRecomputeRowHeights({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
contactResults: { isLoading: false, results: [] },
|
||||||
messageResults: { isLoading: false, results: [fakeMessage()] },
|
messageResults: { isLoading: false, results: [fakeMessage()] },
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns true if the number of results in a section changes', () => {
|
it('returns true if the number of results in a section changes', () => {
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
|
||||||
messageResults: { isLoading: false, results: [] },
|
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.isTrue(
|
assert.isTrue(
|
||||||
helper.shouldRecomputeRowHeights({
|
helper.shouldRecomputeRowHeights({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation()],
|
results: [getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: true },
|
|
||||||
messageResults: { isLoading: true },
|
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'bar',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -560,21 +436,15 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
it('returns correct conversation at given index', () => {
|
it('returns correct conversation at given index', () => {
|
||||||
const expected = getDefaultConversation();
|
const expected = getDefaultConversation();
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [expected, getDefaultConversation()],
|
results: [expected, getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
|
||||||
messageResults: {
|
messageResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
||||||
},
|
},
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
helper.getConversationAndMessageAtIndex(0)?.conversationId,
|
helper.getConversationAndMessageAtIndex(0)?.conversationId,
|
||||||
|
@ -585,6 +455,7 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
it('returns correct contact at given index', () => {
|
it('returns correct contact at given index', () => {
|
||||||
const expected = getDefaultConversation();
|
const expected = getDefaultConversation();
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
|
@ -597,12 +468,6 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
||||||
},
|
},
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
helper.getConversationAndMessageAtIndex(2)?.conversationId,
|
helper.getConversationAndMessageAtIndex(2)?.conversationId,
|
||||||
|
@ -613,21 +478,15 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
it('returns correct message at given index', () => {
|
it('returns correct message at given index', () => {
|
||||||
const expected = fakeMessage();
|
const expected = fakeMessage();
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
|
||||||
messageResults: {
|
messageResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [fakeMessage(), fakeMessage(), expected],
|
results: [fakeMessage(), fakeMessage(), expected],
|
||||||
},
|
},
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
helper.getConversationAndMessageAtIndex(4)?.messageId,
|
helper.getConversationAndMessageAtIndex(4)?.messageId,
|
||||||
|
@ -638,18 +497,13 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
it('returns correct message at given index skipping not loaded results', () => {
|
it('returns correct message at given index skipping not loaded results', () => {
|
||||||
const expected = fakeMessage();
|
const expected = fakeMessage();
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: { isLoading: true },
|
conversationResults: { isLoading: true },
|
||||||
contactResults: { isLoading: true },
|
contactResults: { isLoading: true },
|
||||||
messageResults: {
|
messageResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [fakeMessage(), expected, fakeMessage()],
|
results: [fakeMessage(), expected, fakeMessage()],
|
||||||
},
|
},
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
helper.getConversationAndMessageAtIndex(1)?.messageId,
|
helper.getConversationAndMessageAtIndex(1)?.messageId,
|
||||||
|
@ -659,21 +513,15 @@ describe('LeftPaneSearchHelper', () => {
|
||||||
|
|
||||||
it('returns undefined if search candidate with given index does not exist', () => {
|
it('returns undefined if search candidate with given index does not exist', () => {
|
||||||
const helper = new LeftPaneSearchHelper({
|
const helper = new LeftPaneSearchHelper({
|
||||||
|
...baseSearchHelperArgs,
|
||||||
conversationResults: {
|
conversationResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [getDefaultConversation(), getDefaultConversation()],
|
results: [getDefaultConversation(), getDefaultConversation()],
|
||||||
},
|
},
|
||||||
contactResults: { isLoading: false, results: [] },
|
|
||||||
messageResults: {
|
messageResults: {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
results: [fakeMessage(), fakeMessage(), fakeMessage()],
|
||||||
},
|
},
|
||||||
isSearchingGlobally: true,
|
|
||||||
searchTerm: 'foo',
|
|
||||||
primarySendsSms: false,
|
|
||||||
searchConversation: undefined,
|
|
||||||
searchDisabled: false,
|
|
||||||
startSearchCounter: 0,
|
|
||||||
});
|
});
|
||||||
assert.isUndefined(
|
assert.isUndefined(
|
||||||
helper.getConversationAndMessageAtIndex(100)?.messageId
|
helper.getConversationAndMessageAtIndex(100)?.messageId
|
||||||
|
|
|
@ -64,6 +64,17 @@ type CommandRunnerType = (
|
||||||
|
|
||||||
const COMMANDS = new Map<string, CommandRunnerType>();
|
const COMMANDS = new Map<string, CommandRunnerType>();
|
||||||
|
|
||||||
|
function filterConversationsByUnread(
|
||||||
|
conversations: ReadonlyArray<ConversationType>,
|
||||||
|
includeMuted: boolean
|
||||||
|
): Array<ConversationType> {
|
||||||
|
return conversations.filter(conversation => {
|
||||||
|
return hasUnread(
|
||||||
|
countConversationUnreadStats(conversation, { includeMuted })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
COMMANDS.set('serviceIdEndsWith', (conversations, query) => {
|
COMMANDS.set('serviceIdEndsWith', (conversations, query) => {
|
||||||
return conversations.filter(convo => convo.serviceId?.endsWith(query));
|
return conversations.filter(convo => convo.serviceId?.endsWith(query));
|
||||||
});
|
});
|
||||||
|
@ -95,11 +106,7 @@ COMMANDS.set('unread', (conversations, query) => {
|
||||||
/^(?:m|muted)$/i.test(query) ||
|
/^(?:m|muted)$/i.test(query) ||
|
||||||
window.storage.get('badge-count-muted-conversations') ||
|
window.storage.get('badge-count-muted-conversations') ||
|
||||||
false;
|
false;
|
||||||
return conversations.filter(conversation => {
|
return filterConversationsByUnread(conversations, includeMuted);
|
||||||
return hasUnread(
|
|
||||||
countConversationUnreadStats(conversation, { includeMuted })
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// See https://fusejs.io/examples.html#extended-search for
|
// See https://fusejs.io/examples.html#extended-search for
|
||||||
|
@ -157,14 +164,24 @@ function sortAlphabetically(a: ConversationType, b: ConversationType) {
|
||||||
export function filterAndSortConversations(
|
export function filterAndSortConversations(
|
||||||
conversations: ReadonlyArray<ConversationType>,
|
conversations: ReadonlyArray<ConversationType>,
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
regionCode: string | undefined
|
regionCode: string | undefined,
|
||||||
|
filterByUnread: boolean = false
|
||||||
): Array<ConversationType> {
|
): Array<ConversationType> {
|
||||||
|
const filteredConversations = filterByUnread
|
||||||
|
? filterConversationsByUnread(conversations, true)
|
||||||
|
: conversations;
|
||||||
|
|
||||||
if (searchTerm.length) {
|
if (searchTerm.length) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
const withoutUnknownAndFiltered = filteredConversations.filter(
|
||||||
|
item => item.titleNoDefault
|
||||||
|
);
|
||||||
|
|
||||||
const withoutUnknown = conversations.filter(item => item.titleNoDefault);
|
return searchConversations(
|
||||||
|
withoutUnknownAndFiltered,
|
||||||
return searchConversations(withoutUnknown, searchTerm, regionCode)
|
searchTerm,
|
||||||
|
regionCode
|
||||||
|
)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const { activeAt: aActiveAt = 0, left: aLeft = false } = a.item;
|
const { activeAt: aActiveAt = 0, left: aLeft = false } = a.item;
|
||||||
|
@ -190,7 +207,7 @@ export function filterAndSortConversations(
|
||||||
.map(result => result.item);
|
.map(result => result.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
return conversations.concat().sort((a, b) => {
|
return filteredConversations.concat().sort((a, b) => {
|
||||||
const aScore = a.activeAt ?? 0;
|
const aScore = a.activeAt ?? 0;
|
||||||
const bScore = b.activeAt ?? 0;
|
const bScore = b.activeAt ?? 0;
|
||||||
const score = bScore - aScore;
|
const score = bScore - aScore;
|
||||||
|
|
Loading…
Reference in a new issue