Sort by recency then alphabetically everywhere
This commit is contained in:
parent
9aff86f02b
commit
53ae88c777
11 changed files with 195 additions and 177 deletions
|
@ -8,7 +8,7 @@ import type { ListRowProps } from 'react-virtualized';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
import { ToastType } from '../types/Toast';
|
import { ToastType } from '../types/Toast';
|
||||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
import { filterAndSortConversations } from '../util/filterAndSortConversations';
|
||||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||||
import type { GroupListItemConversationType } from './conversationList/GroupListItem';
|
import type { GroupListItemConversationType } from './conversationList/GroupListItem';
|
||||||
import {
|
import {
|
||||||
|
@ -56,7 +56,7 @@ export function AddUserToAnotherGroupModal({
|
||||||
}: Props): JSX.Element | null {
|
}: Props): JSX.Element | null {
|
||||||
const [searchTerm, setSearchTerm] = React.useState('');
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
const [filteredConversations, setFilteredConversations] = React.useState(
|
const [filteredConversations, setFilteredConversations] = React.useState(
|
||||||
filterAndSortConversationsByRecent(candidateConversations, '', undefined)
|
filterAndSortConversations(candidateConversations, '', undefined)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [selectedGroupId, setSelectedGroupId] = React.useState<
|
const [selectedGroupId, setSelectedGroupId] = React.useState<
|
||||||
|
@ -78,7 +78,7 @@ export function AddUserToAnotherGroupModal({
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setFilteredConversations(
|
setFilteredConversations(
|
||||||
filterAndSortConversationsByRecent(
|
filterAndSortConversations(
|
||||||
candidateConversations,
|
candidateConversations,
|
||||||
normalizedSearchTerm,
|
normalizedSearchTerm,
|
||||||
regionCode
|
regionCode
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { List } from 'react-virtualized';
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { LocalizerType } from '../types/I18N';
|
import type { LocalizerType } from '../types/I18N';
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
import { filterAndSortConversations } from '../util/filterAndSortConversations';
|
||||||
import { NavSidebarSearchHeader } from './NavSidebar';
|
import { NavSidebarSearchHeader } from './NavSidebar';
|
||||||
import { ListTile } from './ListTile';
|
import { ListTile } from './ListTile';
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
@ -89,11 +89,7 @@ export function CallsNewCall({
|
||||||
if (query === '') {
|
if (query === '') {
|
||||||
return activeConversations;
|
return activeConversations;
|
||||||
}
|
}
|
||||||
return filterAndSortConversationsByRecent(
|
return filterAndSortConversations(activeConversations, query, regionCode);
|
||||||
activeConversations,
|
|
||||||
query,
|
|
||||||
regionCode
|
|
||||||
);
|
|
||||||
}, [activeConversations, query, regionCode]);
|
}, [activeConversations, query, regionCode]);
|
||||||
|
|
||||||
const [groupConversations, directConversations] = useMemo(() => {
|
const [groupConversations, directConversations] = useMemo(() => {
|
||||||
|
|
|
@ -23,7 +23,7 @@ import type { LocalizerType, ThemeType } from '../types/Util';
|
||||||
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea';
|
import type { SmartCompositionTextAreaProps } from '../state/smart/CompositionTextArea';
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
import { StagedLinkPreview } from './conversation/StagedLinkPreview';
|
||||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
import { filterAndSortConversations } from '../util/filterAndSortConversations';
|
||||||
import {
|
import {
|
||||||
shouldNeverBeCalled,
|
shouldNeverBeCalled,
|
||||||
asyncShouldNeverBeCalled,
|
asyncShouldNeverBeCalled,
|
||||||
|
@ -96,7 +96,7 @@ export function ForwardMessagesModal({
|
||||||
>([]);
|
>([]);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [filteredConversations, setFilteredConversations] = useState(
|
const [filteredConversations, setFilteredConversations] = useState(
|
||||||
filterAndSortConversationsByRecent(candidateConversations, '', regionCode)
|
filterAndSortConversations(candidateConversations, '', regionCode)
|
||||||
);
|
);
|
||||||
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
const [isEditingMessage, setIsEditingMessage] = useState(false);
|
||||||
const [cannotMessage, setCannotMessage] = useState(false);
|
const [cannotMessage, setCannotMessage] = useState(false);
|
||||||
|
@ -169,7 +169,7 @@ export function ForwardMessagesModal({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setFilteredConversations(
|
setFilteredConversations(
|
||||||
filterAndSortConversationsByRecent(
|
filterAndSortConversations(
|
||||||
candidateConversations,
|
candidateConversations,
|
||||||
normalizedSearchTerm,
|
normalizedSearchTerm,
|
||||||
regionCode
|
regionCode
|
||||||
|
|
|
@ -5,7 +5,7 @@ import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { noop, sortBy } from 'lodash';
|
import { noop, sortBy } from 'lodash';
|
||||||
|
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
import { filterAndSortConversations } from '../util/filterAndSortConversations';
|
||||||
|
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import type { ConversationWithStoriesType } from '../state/selectors/conversations';
|
import type { ConversationWithStoriesType } from '../state/selectors/conversations';
|
||||||
|
@ -175,11 +175,7 @@ export function SendStoryModal({
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
const [filteredConversations, setFilteredConversations] = useState(
|
const [filteredConversations, setFilteredConversations] = useState(
|
||||||
filterAndSortConversationsByRecent(
|
filterAndSortConversations(groupConversations, searchTerm, undefined)
|
||||||
groupConversations,
|
|
||||||
searchTerm,
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const normalizedSearchTerm = searchTerm.trim();
|
const normalizedSearchTerm = searchTerm.trim();
|
||||||
|
@ -187,7 +183,7 @@ export function SendStoryModal({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setFilteredConversations(
|
setFilteredConversations(
|
||||||
filterAndSortConversationsByRecent(
|
filterAndSortConversations(
|
||||||
groupConversations,
|
groupConversations,
|
||||||
normalizedSearchTerm,
|
normalizedSearchTerm,
|
||||||
undefined
|
undefined
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { MY_STORY_ID, getStoryDistributionListName } from '../types/Stories';
|
||||||
import { PagedModal, ModalPage } from './Modal';
|
import { PagedModal, ModalPage } from './Modal';
|
||||||
import { SearchInput } from './SearchInput';
|
import { SearchInput } from './SearchInput';
|
||||||
import { StoryDistributionListName } from './StoryDistributionListName';
|
import { StoryDistributionListName } from './StoryDistributionListName';
|
||||||
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
|
import { filterAndSortConversations } from '../util/filterAndSortConversations';
|
||||||
import { isNotNil } from '../util/isNotNil';
|
import { isNotNil } from '../util/isNotNil';
|
||||||
import {
|
import {
|
||||||
shouldNeverBeCalled,
|
shouldNeverBeCalled,
|
||||||
|
@ -89,7 +89,7 @@ function filterConversations(
|
||||||
conversations: ReadonlyArray<ConversationType>,
|
conversations: ReadonlyArray<ConversationType>,
|
||||||
searchTerm: string
|
searchTerm: string
|
||||||
) {
|
) {
|
||||||
return filterAndSortConversationsByRecent(
|
return filterAndSortConversations(
|
||||||
conversations,
|
conversations,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
undefined
|
undefined
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { missingCaseError } from '../../../../util/missingCaseError';
|
||||||
import type { LookupConversationWithoutServiceIdActionsType } from '../../../../util/lookupConversationWithoutServiceId';
|
import type { LookupConversationWithoutServiceIdActionsType } from '../../../../util/lookupConversationWithoutServiceId';
|
||||||
import { parseAndFormatPhoneNumber } from '../../../../util/libphonenumberInstance';
|
import { parseAndFormatPhoneNumber } from '../../../../util/libphonenumberInstance';
|
||||||
import type { ParsedE164Type } from '../../../../util/libphonenumberInstance';
|
import type { ParsedE164Type } from '../../../../util/libphonenumberInstance';
|
||||||
import { filterAndSortConversationsByRecent } from '../../../../util/filterAndSortConversations';
|
import { filterAndSortConversations } from '../../../../util/filterAndSortConversations';
|
||||||
import type { ConversationType } from '../../../../state/ducks/conversations';
|
import type { ConversationType } from '../../../../state/ducks/conversations';
|
||||||
import type {
|
import type {
|
||||||
UUIDFetchStateKeyType,
|
UUIDFetchStateKeyType,
|
||||||
|
@ -140,13 +140,13 @@ export function ChooseGroupMembersModal({
|
||||||
const canContinue = Boolean(selectedContacts.length);
|
const canContinue = Boolean(selectedContacts.length);
|
||||||
|
|
||||||
const [filteredContacts, setFilteredContacts] = useState(
|
const [filteredContacts, setFilteredContacts] = useState(
|
||||||
filterAndSortConversationsByRecent(candidateContacts, '', regionCode)
|
filterAndSortConversations(candidateContacts, '', regionCode)
|
||||||
);
|
);
|
||||||
const normalizedSearchTerm = searchTerm.trim();
|
const normalizedSearchTerm = searchTerm.trim();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setFilteredContacts(
|
setFilteredContacts(
|
||||||
filterAndSortConversationsByRecent(
|
filterAndSortConversations(
|
||||||
candidateContacts,
|
candidateContacts,
|
||||||
normalizedSearchTerm,
|
normalizedSearchTerm,
|
||||||
regionCode
|
regionCode
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { debounce, omit, reject } from 'lodash';
|
||||||
|
|
||||||
import type { ReadonlyDeep } from 'type-fest';
|
import type { ReadonlyDeep } from 'type-fest';
|
||||||
import type { StateType as RootStateType } from '../reducer';
|
import type { StateType as RootStateType } from '../reducer';
|
||||||
import { filterAndSortConversationsByRecent } from '../../util/filterAndSortConversations';
|
import { filterAndSortConversations } from '../../util/filterAndSortConversations';
|
||||||
import type {
|
import type {
|
||||||
ClientSearchResultMessageType,
|
ClientSearchResultMessageType,
|
||||||
ClientInterface,
|
ClientInterface,
|
||||||
|
@ -337,8 +337,7 @@ async function queryConversationsAndContacts(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const searchResults: Array<ConversationType> =
|
const searchResults: Array<ConversationType> = filterAndSortConversations(
|
||||||
filterAndSortConversationsByRecent(
|
|
||||||
visibleConversations,
|
visibleConversations,
|
||||||
normalizedQuery,
|
normalizedQuery,
|
||||||
regionCode
|
regionCode
|
||||||
|
|
|
@ -30,10 +30,7 @@ import { deconstructLookup } from '../../util/deconstructLookup';
|
||||||
import type { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
import type { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
||||||
import { assertDev } from '../../util/assert';
|
import { assertDev } from '../../util/assert';
|
||||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||||
import {
|
import { filterAndSortConversations } from '../../util/filterAndSortConversations';
|
||||||
filterAndSortConversationsAlphabetically,
|
|
||||||
filterAndSortConversationsByRecent,
|
|
||||||
} from '../../util/filterAndSortConversations';
|
|
||||||
import type { ContactNameColorType } from '../../types/Colors';
|
import type { ContactNameColorType } from '../../types/Colors';
|
||||||
import { ContactNameColors } from '../../types/Colors';
|
import { ContactNameColors } from '../../types/Colors';
|
||||||
import type { AvatarDataType } from '../../types/Avatar';
|
import type { AvatarDataType } from '../../types/Avatar';
|
||||||
|
@ -724,11 +721,7 @@ export const getFilteredComposeContacts = createSelector(
|
||||||
contacts: ReadonlyArray<ConversationType>,
|
contacts: ReadonlyArray<ConversationType>,
|
||||||
regionCode: string | undefined
|
regionCode: string | undefined
|
||||||
): Array<ConversationType> => {
|
): Array<ConversationType> => {
|
||||||
return filterAndSortConversationsAlphabetically(
|
return filterAndSortConversations(contacts, searchTerm, regionCode);
|
||||||
contacts,
|
|
||||||
searchTerm,
|
|
||||||
regionCode
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -750,18 +743,16 @@ export const getFilteredComposeGroups = createSelector(
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
> => {
|
> => {
|
||||||
return filterAndSortConversationsAlphabetically(
|
return filterAndSortConversations(groups, searchTerm, regionCode).map(
|
||||||
groups,
|
group => ({
|
||||||
searchTerm,
|
|
||||||
regionCode
|
|
||||||
).map(group => ({
|
|
||||||
...group,
|
...group,
|
||||||
// we don't disable groups when composing, already filtered
|
// we don't disable groups when composing, already filtered
|
||||||
disabledReason: undefined,
|
disabledReason: undefined,
|
||||||
// should always be populated for a group
|
// should always be populated for a group
|
||||||
membersCount: group.membersCount ?? 0,
|
membersCount: group.membersCount ?? 0,
|
||||||
memberships: group.memberships ?? [],
|
memberships: group.memberships ?? [],
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -769,7 +760,7 @@ export const getFilteredCandidateContactsForNewGroup = createSelector(
|
||||||
getCandidateContactsForNewGroup,
|
getCandidateContactsForNewGroup,
|
||||||
getNormalizedComposerConversationSearchTerm,
|
getNormalizedComposerConversationSearchTerm,
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
filterAndSortConversationsByRecent
|
filterAndSortConversations
|
||||||
);
|
);
|
||||||
|
|
||||||
const getGroupCreationComposerState = createSelector(
|
const getGroupCreationComposerState = createSelector(
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
getAllConversations,
|
getAllConversations,
|
||||||
getConversationSelector,
|
getConversationSelector,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import { filterAndSortConversationsByRecent } from '../../util/filterAndSortConversations';
|
import { filterAndSortConversations } from '../../util/filterAndSortConversations';
|
||||||
import type {
|
import type {
|
||||||
CallHistoryFilter,
|
CallHistoryFilter,
|
||||||
CallHistoryFilterOptions,
|
CallHistoryFilterOptions,
|
||||||
|
@ -44,7 +44,7 @@ function getCallHistoryFilter(
|
||||||
return conversation.removalStage == null;
|
return conversation.removalStage == null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredConversations = filterAndSortConversationsByRecent(
|
const filteredConversations = filterAndSortConversations(
|
||||||
currentConversations,
|
currentConversations,
|
||||||
query,
|
query,
|
||||||
regionCode
|
regionCode
|
||||||
|
|
|
@ -2,90 +2,138 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
import { pick } from 'lodash';
|
||||||
import { getDefaultConversation } from '../helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../helpers/getDefaultConversation';
|
||||||
|
import { filterAndSortConversations } from '../../util/filterAndSortConversations';
|
||||||
|
import type { ConversationType } from '../../state/ducks/conversations';
|
||||||
|
|
||||||
import {
|
type CheckProps = Pick<ConversationType, 'title' | 'activeAt' | 'e164'>;
|
||||||
filterAndSortConversationsAlphabetically,
|
|
||||||
filterAndSortConversationsByRecent,
|
function check({
|
||||||
} from '../../util/filterAndSortConversations';
|
searchTerm,
|
||||||
|
input,
|
||||||
|
expected,
|
||||||
|
}: {
|
||||||
|
searchTerm: string;
|
||||||
|
input: Array<CheckProps>;
|
||||||
|
expected: Array<CheckProps>;
|
||||||
|
}) {
|
||||||
|
const conversations = input.map(props => {
|
||||||
|
return getDefaultConversation(props);
|
||||||
|
});
|
||||||
|
const results = filterAndSortConversations(conversations, searchTerm, 'US');
|
||||||
|
const actual = results.map(convo => {
|
||||||
|
return pick(convo, 'title', 'activeAt');
|
||||||
|
});
|
||||||
|
assert.sameDeepMembers(actual, expected);
|
||||||
|
}
|
||||||
|
|
||||||
describe('filterAndSortConversations', () => {
|
describe('filterAndSortConversations', () => {
|
||||||
const conversations = [
|
it('finds a conversation by title', () => {
|
||||||
getDefaultConversation({
|
check({
|
||||||
title: '+16505551234',
|
searchTerm: 'yes',
|
||||||
activeAt: 1,
|
input: [{ title: 'no' }, { title: 'yes' }, { title: 'no' }],
|
||||||
}),
|
expected: [{ title: 'yes' }],
|
||||||
getDefaultConversation({
|
|
||||||
title: 'The Abraham Lincoln Club',
|
|
||||||
activeAt: 4,
|
|
||||||
}),
|
|
||||||
getDefaultConversation({
|
|
||||||
title: 'Boxing Club',
|
|
||||||
activeAt: 3,
|
|
||||||
}),
|
|
||||||
getDefaultConversation({
|
|
||||||
title: 'Not recent',
|
|
||||||
}),
|
|
||||||
getDefaultConversation({
|
|
||||||
title: 'George Washington',
|
|
||||||
e164: '+16505559876',
|
|
||||||
activeAt: 2,
|
|
||||||
}),
|
|
||||||
getDefaultConversation({
|
|
||||||
title: 'A long long long title ending with burrito',
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
it('filterAndSortConversationsByRecent sorts by recency when no search term is provided', () => {
|
|
||||||
const titles = filterAndSortConversationsByRecent(
|
|
||||||
conversations,
|
|
||||||
'',
|
|
||||||
'US'
|
|
||||||
).map(contact => contact.title);
|
|
||||||
assert.sameOrderedMembers(titles, [
|
|
||||||
'The Abraham Lincoln Club',
|
|
||||||
'Boxing Club',
|
|
||||||
'George Washington',
|
|
||||||
'+16505551234',
|
|
||||||
'Not recent',
|
|
||||||
'A long long long title ending with burrito',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filterAndSortConversationsAlphabetically sorts by title when no search term is provided', () => {
|
|
||||||
const titles = filterAndSortConversationsAlphabetically(
|
|
||||||
conversations,
|
|
||||||
'',
|
|
||||||
'US'
|
|
||||||
).map(contact => contact.title);
|
|
||||||
assert.sameOrderedMembers(titles, [
|
|
||||||
'A long long long title ending with burrito',
|
|
||||||
'Boxing Club',
|
|
||||||
'George Washington',
|
|
||||||
'Not recent',
|
|
||||||
'The Abraham Lincoln Club',
|
|
||||||
'+16505551234',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('filterAndSortConversationsAlphabetically sorts by title when a search term is provided', () => {
|
|
||||||
const titles = filterAndSortConversationsAlphabetically(
|
|
||||||
conversations,
|
|
||||||
'club',
|
|
||||||
'US'
|
|
||||||
).map(contact => contact.title);
|
|
||||||
assert.sameOrderedMembers(titles, [
|
|
||||||
'Boxing Club',
|
|
||||||
'The Abraham Lincoln Club',
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finds a conversation when the search term is at the end of a long title', () => {
|
it('finds a conversation when the search term is at the end of a long title', () => {
|
||||||
const titles = filterAndSortConversationsByRecent(
|
check({
|
||||||
conversations,
|
searchTerm: 'burrito',
|
||||||
'burrito',
|
input: [
|
||||||
'US'
|
{ title: 'no' },
|
||||||
).map(convo => convo.title);
|
{
|
||||||
assert.deepEqual(titles, ['A long long long title ending with burrito']);
|
title: 'A long long long title ending with burrito',
|
||||||
|
},
|
||||||
|
{ title: 'no' },
|
||||||
|
],
|
||||||
|
expected: [
|
||||||
|
{
|
||||||
|
title: 'A long long long title ending with burrito',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('finds a conversation by phone number', () => {
|
||||||
|
check({
|
||||||
|
searchTerm: '9876',
|
||||||
|
input: [
|
||||||
|
{ title: 'no' },
|
||||||
|
{ title: 'yes', e164: '+16505559876' },
|
||||||
|
{ title: 'no' },
|
||||||
|
],
|
||||||
|
expected: [{ title: 'yes' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('no search term', () => {
|
||||||
|
it('sorts by recency first', () => {
|
||||||
|
check({
|
||||||
|
searchTerm: '',
|
||||||
|
input: [
|
||||||
|
{ title: 'B', activeAt: 2 },
|
||||||
|
{ title: 'A', activeAt: 1 },
|
||||||
|
{ title: 'C', activeAt: 3 },
|
||||||
|
],
|
||||||
|
expected: [
|
||||||
|
{ title: 'C', activeAt: 3 },
|
||||||
|
{ title: 'B', activeAt: 2 },
|
||||||
|
{ title: 'A', activeAt: 1 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to alphabetically', () => {
|
||||||
|
check({
|
||||||
|
searchTerm: '',
|
||||||
|
input: [
|
||||||
|
{ title: 'B', activeAt: 2 },
|
||||||
|
{ title: 'A', activeAt: 2 },
|
||||||
|
{ title: 'C', activeAt: 3 },
|
||||||
|
],
|
||||||
|
expected: [
|
||||||
|
{ title: 'C', activeAt: 3 },
|
||||||
|
{ title: 'A', activeAt: 2 },
|
||||||
|
{ title: 'B', activeAt: 2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with search term', () => {
|
||||||
|
it('sorts by recency first', () => {
|
||||||
|
check({
|
||||||
|
searchTerm: 'yes',
|
||||||
|
input: [
|
||||||
|
{ title: 'no' },
|
||||||
|
{ title: 'yes B', activeAt: 2 },
|
||||||
|
{ title: 'yes A', activeAt: 1 },
|
||||||
|
{ title: 'yes C', activeAt: 3 },
|
||||||
|
],
|
||||||
|
expected: [
|
||||||
|
{ title: 'yes C', activeAt: 3 },
|
||||||
|
{ title: 'yes B', activeAt: 2 },
|
||||||
|
{ title: 'yes A', activeAt: 1 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to alphabetically', () => {
|
||||||
|
check({
|
||||||
|
searchTerm: 'yes',
|
||||||
|
input: [
|
||||||
|
{ title: 'no' },
|
||||||
|
{ title: 'yes B', activeAt: 2 },
|
||||||
|
{ title: 'yes A', activeAt: 2 },
|
||||||
|
{ title: 'yes C', activeAt: 3 },
|
||||||
|
],
|
||||||
|
expected: [
|
||||||
|
{ title: 'yes C', activeAt: 3 },
|
||||||
|
{ title: 'yes A', activeAt: 2 },
|
||||||
|
{ title: 'yes B', activeAt: 2 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type Fuse from 'fuse.js';
|
import type Fuse from 'fuse.js';
|
||||||
|
|
||||||
import type { ConversationType } from '../state/ducks/conversations';
|
import type { ConversationType } from '../state/ducks/conversations';
|
||||||
import { parseAndFormatPhoneNumber } from './libphonenumberInstance';
|
import { parseAndFormatPhoneNumber } from './libphonenumberInstance';
|
||||||
import { WEEK } from './durations';
|
import { WEEK } from './durations';
|
||||||
|
@ -129,7 +128,27 @@ function searchConversations(
|
||||||
return index.search(extendedSearchTerm);
|
return index.search(extendedSearchTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterAndSortConversationsByRecent(
|
function startsWithLetter(title: string) {
|
||||||
|
// Uses \p, the unicode character class escape, to check if a the first character is a
|
||||||
|
// letter
|
||||||
|
return /^\p{Letter}/u.test(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortAlphabetically(a: ConversationType, b: ConversationType) {
|
||||||
|
// Sort alphabetically with conversations starting with a letter first (and phone
|
||||||
|
// numbers last)
|
||||||
|
const aStartsWithLetter = startsWithLetter(a.title);
|
||||||
|
const bStartsWithLetter = startsWithLetter(b.title);
|
||||||
|
if (aStartsWithLetter && !bStartsWithLetter) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (!aStartsWithLetter && bStartsWithLetter) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return a.title.localeCompare(b.title);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterAndSortConversations(
|
||||||
conversations: ReadonlyArray<ConversationType>,
|
conversations: ReadonlyArray<ConversationType>,
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
regionCode: string | undefined
|
regionCode: string | undefined
|
||||||
|
@ -156,53 +175,22 @@ export function filterAndSortConversationsByRecent(
|
||||||
(b.score ?? 0) +
|
(b.score ?? 0) +
|
||||||
(bLeft ? LEFT_GROUP_PENALTY : 0);
|
(bLeft ? LEFT_GROUP_PENALTY : 0);
|
||||||
|
|
||||||
return aScore - bScore;
|
const activeScore = aScore - bScore;
|
||||||
|
if (activeScore !== 0) {
|
||||||
|
return activeScore;
|
||||||
|
}
|
||||||
|
return sortAlphabetically(a.item, b.item);
|
||||||
})
|
})
|
||||||
.map(result => result.item);
|
.map(result => result.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
return conversations.concat().sort((a, b) => {
|
return conversations.concat().sort((a, b) => {
|
||||||
if (a.activeAt && b.activeAt) {
|
const aScore = a.activeAt ?? 0;
|
||||||
return a.activeAt > b.activeAt ? -1 : 1;
|
const bScore = b.activeAt ?? 0;
|
||||||
|
const score = bScore - aScore;
|
||||||
|
if (score !== 0) {
|
||||||
|
return score;
|
||||||
}
|
}
|
||||||
|
return sortAlphabetically(a, b);
|
||||||
return a.activeAt && !b.activeAt ? -1 : 1;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startsWithLetter(title: string) {
|
|
||||||
// Uses \p, the unicode character class escape, to check if a the first character is a
|
|
||||||
// letter
|
|
||||||
return /^\p{Letter}/u.test(title);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortAlphabetically(a: ConversationType, b: ConversationType) {
|
|
||||||
// Sort alphabetically with conversations starting with a letter first (and phone
|
|
||||||
// numbers last)
|
|
||||||
const aStartsWithLetter = startsWithLetter(a.title);
|
|
||||||
const bStartsWithLetter = startsWithLetter(b.title);
|
|
||||||
if (aStartsWithLetter && !bStartsWithLetter) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!aStartsWithLetter && bStartsWithLetter) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return a.title.localeCompare(b.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterAndSortConversationsAlphabetically(
|
|
||||||
conversations: ReadonlyArray<ConversationType>,
|
|
||||||
searchTerm: string,
|
|
||||||
regionCode: string | undefined
|
|
||||||
): Array<ConversationType> {
|
|
||||||
if (searchTerm.length) {
|
|
||||||
const withoutUnknown = conversations.filter(item => item.titleNoDefault);
|
|
||||||
|
|
||||||
return searchConversations(withoutUnknown, searchTerm, regionCode)
|
|
||||||
.slice()
|
|
||||||
.map(result => result.item)
|
|
||||||
.sort(sortAlphabetically);
|
|
||||||
}
|
|
||||||
|
|
||||||
return conversations.concat().sort(sortAlphabetically);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue