Sort groups you've left to the bottom of search
This commit is contained in:
parent
fcd0a186e0
commit
49bb1321e3
5 changed files with 18 additions and 128 deletions
|
@ -20,7 +20,7 @@ import { useRestoreFocus } from '../../../../hooks/useRestoreFocus';
|
|||
import { missingCaseError } from '../../../../util/missingCaseError';
|
||||
import type { LookupConversationWithoutUuidActionsType } from '../../../../util/lookupConversationWithoutUuid';
|
||||
import { parseAndFormatPhoneNumber } from '../../../../util/libphonenumberInstance';
|
||||
import { filterAndSortConversationsByTitle } from '../../../../util/filterAndSortConversations';
|
||||
import { filterAndSortConversationsByRecent } from '../../../../util/filterAndSortConversations';
|
||||
import type { ConversationType } from '../../../../state/ducks/conversations';
|
||||
import type { PreferredBadgeSelectorType } from '../../../../state/selectors/badges';
|
||||
import type {
|
||||
|
@ -114,13 +114,13 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
|||
const canContinue = Boolean(selectedContacts.length);
|
||||
|
||||
const [filteredContacts, setFilteredContacts] = useState(
|
||||
filterAndSortConversationsByTitle(candidateContacts, '', regionCode)
|
||||
filterAndSortConversationsByRecent(candidateContacts, '', regionCode)
|
||||
);
|
||||
const normalizedSearchTerm = searchTerm.trim();
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setFilteredContacts(
|
||||
filterAndSortConversationsByTitle(
|
||||
filterAndSortConversationsByRecent(
|
||||
candidateContacts,
|
||||
normalizedSearchTerm,
|
||||
regionCode
|
||||
|
|
|
@ -31,7 +31,7 @@ import type { PropsDataType as TimelinePropsType } from '../../components/conver
|
|||
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||
import { assert } from '../../util/assert';
|
||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||
import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConversations';
|
||||
import { filterAndSortConversationsByRecent } from '../../util/filterAndSortConversations';
|
||||
import type { ContactNameColorType } from '../../types/Colors';
|
||||
import { ContactNameColors } from '../../types/Colors';
|
||||
import type { AvatarDataType } from '../../types/Avatar';
|
||||
|
@ -523,7 +523,7 @@ export const getFilteredComposeContacts = createSelector(
|
|||
contacts: Array<ConversationType>,
|
||||
regionCode: string | undefined
|
||||
): Array<ConversationType> => {
|
||||
return filterAndSortConversationsByTitle(contacts, searchTerm, regionCode);
|
||||
return filterAndSortConversationsByRecent(contacts, searchTerm, regionCode);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -536,7 +536,7 @@ export const getFilteredComposeGroups = createSelector(
|
|||
groups: Array<ConversationType>,
|
||||
regionCode: string | undefined
|
||||
): Array<ConversationType> => {
|
||||
return filterAndSortConversationsByTitle(groups, searchTerm, regionCode);
|
||||
return filterAndSortConversationsByRecent(groups, searchTerm, regionCode);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -544,7 +544,7 @@ export const getFilteredCandidateContactsForNewGroup = createSelector(
|
|||
getCandidateContactsForNewGroup,
|
||||
getNormalizedComposerConversationSearchTerm,
|
||||
getRegionCode,
|
||||
filterAndSortConversationsByTitle
|
||||
filterAndSortConversationsByRecent
|
||||
);
|
||||
|
||||
const getGroupCreationComposerState = createSelector(
|
||||
|
|
|
@ -965,10 +965,10 @@ describe('both/state/selectors/conversations', () => {
|
|||
|
||||
const ids = result.map(contact => contact.id);
|
||||
assert.deepEqual(ids, [
|
||||
'our-conversation-id',
|
||||
'convo-1',
|
||||
'convo-5',
|
||||
'convo-6',
|
||||
'our-conversation-id',
|
||||
]);
|
||||
});
|
||||
|
||||
|
|
|
@ -4,90 +4,7 @@
|
|||
import { assert } from 'chai';
|
||||
import { getDefaultConversation } from '../helpers/getDefaultConversation';
|
||||
|
||||
import {
|
||||
filterAndSortConversationsByTitle,
|
||||
filterAndSortConversationsByRecent,
|
||||
} from '../../util/filterAndSortConversations';
|
||||
|
||||
describe('filterAndSortConversationsByTitle', () => {
|
||||
const conversations = [
|
||||
getDefaultConversation({
|
||||
title: '+16505551234',
|
||||
e164: '+16505551234',
|
||||
name: undefined,
|
||||
profileName: undefined,
|
||||
}),
|
||||
getDefaultConversation({
|
||||
name: 'Carlos Santana',
|
||||
title: 'Carlos Santana',
|
||||
e164: '+16505559876',
|
||||
username: 'thisismyusername',
|
||||
}),
|
||||
getDefaultConversation({
|
||||
name: 'Aaron Aardvark',
|
||||
title: 'Aaron Aardvark',
|
||||
}),
|
||||
getDefaultConversation({
|
||||
name: 'Belinda Beetle',
|
||||
title: 'Belinda Beetle',
|
||||
}),
|
||||
getDefaultConversation({
|
||||
name: 'Belinda Zephyr',
|
||||
title: 'Belinda Zephyr',
|
||||
}),
|
||||
];
|
||||
|
||||
it('without a search term, sorts conversations by title (but puts no-name contacts at the bottom)', () => {
|
||||
const titles = filterAndSortConversationsByTitle(
|
||||
conversations,
|
||||
'',
|
||||
'US'
|
||||
).map(contact => contact.title);
|
||||
assert.deepEqual(titles, [
|
||||
'Aaron Aardvark',
|
||||
'Belinda Beetle',
|
||||
'Belinda Zephyr',
|
||||
'Carlos Santana',
|
||||
'+16505551234',
|
||||
]);
|
||||
});
|
||||
|
||||
it('can search for contacts by title', () => {
|
||||
const titles = filterAndSortConversationsByTitle(
|
||||
conversations,
|
||||
'belind',
|
||||
'US'
|
||||
).map(contact => contact.title);
|
||||
assert.sameMembers(titles, ['Belinda Beetle', 'Belinda Zephyr']);
|
||||
});
|
||||
|
||||
it('can search for contacts by phone number (and puts no-name contacts at the bottom)', () => {
|
||||
const titles = filterAndSortConversationsByTitle(
|
||||
conversations,
|
||||
'650555',
|
||||
'US'
|
||||
).map(contact => contact.title);
|
||||
assert.sameMembers(titles, ['Carlos Santana', '+16505551234']);
|
||||
});
|
||||
|
||||
it('can search for contacts by formatted phone number (and puts no-name contacts at the bottom)', () => {
|
||||
const titles = filterAndSortConversationsByTitle(
|
||||
conversations,
|
||||
'(650)555 12-34',
|
||||
'US'
|
||||
).map(contact => contact.title);
|
||||
assert.sameMembers(titles, ['+16505551234']);
|
||||
});
|
||||
|
||||
it('can search for contacts by username', () => {
|
||||
const titles = filterAndSortConversationsByTitle(
|
||||
conversations,
|
||||
'thisis',
|
||||
'US'
|
||||
).map(contact => contact.title);
|
||||
assert.sameMembers(titles, ['Carlos Santana']);
|
||||
});
|
||||
});
|
||||
import { filterAndSortConversationsByRecent } from '../../util/filterAndSortConversations';
|
||||
|
||||
describe('filterAndSortConversationsByRecent', () => {
|
||||
const conversations = [
|
||||
|
|
|
@ -9,6 +9,7 @@ import { WEEK } from './durations';
|
|||
|
||||
// Fuse.js scores have order of 0.01
|
||||
const ACTIVE_AT_SCORE_FACTOR = (1 / WEEK) * 0.01;
|
||||
const LEFT_GROUP_PENALTY = 1;
|
||||
|
||||
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationType> = {
|
||||
// A small-but-nonzero threshold lets us match parts of E164s better, and makes the
|
||||
|
@ -42,8 +43,6 @@ const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationType> = {
|
|||
],
|
||||
};
|
||||
|
||||
const collator = new Intl.Collator();
|
||||
|
||||
const cachedIndices = new WeakMap<
|
||||
ReadonlyArray<ConversationType>,
|
||||
Fuse<ConversationType>
|
||||
|
@ -119,15 +118,19 @@ export function filterAndSortConversationsByRecent(
|
|||
return searchConversations(conversations, searchTerm, regionCode)
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const { activeAt: aActiveAt = 0 } = a.item;
|
||||
const { activeAt: bActiveAt = 0 } = b.item;
|
||||
const { activeAt: aActiveAt = 0, left: aLeft = false } = a.item;
|
||||
const { activeAt: bActiveAt = 0, left: bLeft = false } = b.item;
|
||||
|
||||
// See: https://fusejs.io/api/options.html#includescore
|
||||
// 0 score is a perfect match, 1 - complete mismatch
|
||||
const aScore =
|
||||
(now - aActiveAt) * ACTIVE_AT_SCORE_FACTOR + (a.score ?? 0);
|
||||
(now - aActiveAt) * ACTIVE_AT_SCORE_FACTOR +
|
||||
(a.score ?? 0) +
|
||||
(aLeft ? LEFT_GROUP_PENALTY : 0);
|
||||
const bScore =
|
||||
(now - bActiveAt) * ACTIVE_AT_SCORE_FACTOR + (b.score ?? 0);
|
||||
(now - bActiveAt) * ACTIVE_AT_SCORE_FACTOR +
|
||||
(b.score ?? 0) +
|
||||
(bLeft ? LEFT_GROUP_PENALTY : 0);
|
||||
|
||||
return aScore - bScore;
|
||||
})
|
||||
|
@ -142,33 +145,3 @@ export function filterAndSortConversationsByRecent(
|
|||
return a.activeAt && !b.activeAt ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
export function filterAndSortConversationsByTitle(
|
||||
conversations: ReadonlyArray<ConversationType>,
|
||||
searchTerm: string,
|
||||
regionCode: string | undefined
|
||||
): Array<ConversationType> {
|
||||
if (searchTerm.length) {
|
||||
return searchConversations(conversations, searchTerm, regionCode)
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
return (a.score ?? 0) - (b.score ?? 0);
|
||||
})
|
||||
.map(result => result.item);
|
||||
}
|
||||
|
||||
return conversations.concat().sort((a, b) => {
|
||||
const aHasName = hasName(a);
|
||||
const bHasName = hasName(b);
|
||||
|
||||
if (aHasName === bHasName) {
|
||||
return collator.compare(a.title, b.title);
|
||||
}
|
||||
|
||||
return aHasName && !bHasName ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
function hasName(contact: Readonly<ConversationType>): boolean {
|
||||
return Boolean(contact.name || contact.profileName);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue