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