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 { missingCaseError } from '../../../../util/missingCaseError';
|
||||||
import type { LookupConversationWithoutUuidActionsType } from '../../../../util/lookupConversationWithoutUuid';
|
import type { LookupConversationWithoutUuidActionsType } from '../../../../util/lookupConversationWithoutUuid';
|
||||||
import { parseAndFormatPhoneNumber } from '../../../../util/libphonenumberInstance';
|
import { parseAndFormatPhoneNumber } from '../../../../util/libphonenumberInstance';
|
||||||
import { filterAndSortConversationsByTitle } from '../../../../util/filterAndSortConversations';
|
import { filterAndSortConversationsByRecent } from '../../../../util/filterAndSortConversations';
|
||||||
import type { ConversationType } from '../../../../state/ducks/conversations';
|
import type { ConversationType } from '../../../../state/ducks/conversations';
|
||||||
import type { PreferredBadgeSelectorType } from '../../../../state/selectors/badges';
|
import type { PreferredBadgeSelectorType } from '../../../../state/selectors/badges';
|
||||||
import type {
|
import type {
|
||||||
|
@ -114,13 +114,13 @@ export const ChooseGroupMembersModal: FunctionComponent<PropsType> = ({
|
||||||
const canContinue = Boolean(selectedContacts.length);
|
const canContinue = Boolean(selectedContacts.length);
|
||||||
|
|
||||||
const [filteredContacts, setFilteredContacts] = useState(
|
const [filteredContacts, setFilteredContacts] = useState(
|
||||||
filterAndSortConversationsByTitle(candidateContacts, '', regionCode)
|
filterAndSortConversationsByRecent(candidateContacts, '', regionCode)
|
||||||
);
|
);
|
||||||
const normalizedSearchTerm = searchTerm.trim();
|
const normalizedSearchTerm = searchTerm.trim();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setFilteredContacts(
|
setFilteredContacts(
|
||||||
filterAndSortConversationsByTitle(
|
filterAndSortConversationsByRecent(
|
||||||
candidateContacts,
|
candidateContacts,
|
||||||
normalizedSearchTerm,
|
normalizedSearchTerm,
|
||||||
regionCode
|
regionCode
|
||||||
|
|
|
@ -31,7 +31,7 @@ import type { PropsDataType as TimelinePropsType } from '../../components/conver
|
||||||
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
import type { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||||
import { assert } from '../../util/assert';
|
import { assert } from '../../util/assert';
|
||||||
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
import { isConversationUnregistered } from '../../util/isConversationUnregistered';
|
||||||
import { filterAndSortConversationsByTitle } from '../../util/filterAndSortConversations';
|
import { 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';
|
||||||
|
@ -523,7 +523,7 @@ export const getFilteredComposeContacts = createSelector(
|
||||||
contacts: Array<ConversationType>,
|
contacts: Array<ConversationType>,
|
||||||
regionCode: string | undefined
|
regionCode: string | undefined
|
||||||
): Array<ConversationType> => {
|
): Array<ConversationType> => {
|
||||||
return filterAndSortConversationsByTitle(contacts, searchTerm, regionCode);
|
return filterAndSortConversationsByRecent(contacts, searchTerm, regionCode);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -536,7 +536,7 @@ export const getFilteredComposeGroups = createSelector(
|
||||||
groups: Array<ConversationType>,
|
groups: Array<ConversationType>,
|
||||||
regionCode: string | undefined
|
regionCode: string | undefined
|
||||||
): Array<ConversationType> => {
|
): Array<ConversationType> => {
|
||||||
return filterAndSortConversationsByTitle(groups, searchTerm, regionCode);
|
return filterAndSortConversationsByRecent(groups, searchTerm, regionCode);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -544,7 +544,7 @@ export const getFilteredCandidateContactsForNewGroup = createSelector(
|
||||||
getCandidateContactsForNewGroup,
|
getCandidateContactsForNewGroup,
|
||||||
getNormalizedComposerConversationSearchTerm,
|
getNormalizedComposerConversationSearchTerm,
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
filterAndSortConversationsByTitle
|
filterAndSortConversationsByRecent
|
||||||
);
|
);
|
||||||
|
|
||||||
const getGroupCreationComposerState = createSelector(
|
const getGroupCreationComposerState = createSelector(
|
||||||
|
|
|
@ -965,10 +965,10 @@ describe('both/state/selectors/conversations', () => {
|
||||||
|
|
||||||
const ids = result.map(contact => contact.id);
|
const ids = result.map(contact => contact.id);
|
||||||
assert.deepEqual(ids, [
|
assert.deepEqual(ids, [
|
||||||
|
'our-conversation-id',
|
||||||
'convo-1',
|
'convo-1',
|
||||||
'convo-5',
|
'convo-5',
|
||||||
'convo-6',
|
'convo-6',
|
||||||
'our-conversation-id',
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,90 +4,7 @@
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { getDefaultConversation } from '../helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../helpers/getDefaultConversation';
|
||||||
|
|
||||||
import {
|
import { filterAndSortConversationsByRecent } from '../../util/filterAndSortConversations';
|
||||||
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']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('filterAndSortConversationsByRecent', () => {
|
describe('filterAndSortConversationsByRecent', () => {
|
||||||
const conversations = [
|
const conversations = [
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { WEEK } from './durations';
|
||||||
|
|
||||||
// Fuse.js scores have order of 0.01
|
// Fuse.js scores have order of 0.01
|
||||||
const ACTIVE_AT_SCORE_FACTOR = (1 / WEEK) * 0.01;
|
const ACTIVE_AT_SCORE_FACTOR = (1 / WEEK) * 0.01;
|
||||||
|
const LEFT_GROUP_PENALTY = 1;
|
||||||
|
|
||||||
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationType> = {
|
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationType> = {
|
||||||
// A small-but-nonzero threshold lets us match parts of E164s better, and makes the
|
// 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<
|
const cachedIndices = new WeakMap<
|
||||||
ReadonlyArray<ConversationType>,
|
ReadonlyArray<ConversationType>,
|
||||||
Fuse<ConversationType>
|
Fuse<ConversationType>
|
||||||
|
@ -119,15 +118,19 @@ export function filterAndSortConversationsByRecent(
|
||||||
return searchConversations(conversations, searchTerm, regionCode)
|
return searchConversations(conversations, searchTerm, regionCode)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const { activeAt: aActiveAt = 0 } = a.item;
|
const { activeAt: aActiveAt = 0, left: aLeft = false } = a.item;
|
||||||
const { activeAt: bActiveAt = 0 } = b.item;
|
const { activeAt: bActiveAt = 0, left: bLeft = false } = b.item;
|
||||||
|
|
||||||
// See: https://fusejs.io/api/options.html#includescore
|
// See: https://fusejs.io/api/options.html#includescore
|
||||||
// 0 score is a perfect match, 1 - complete mismatch
|
// 0 score is a perfect match, 1 - complete mismatch
|
||||||
const aScore =
|
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 =
|
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;
|
return aScore - bScore;
|
||||||
})
|
})
|
||||||
|
@ -142,33 +145,3 @@ export function filterAndSortConversationsByRecent(
|
||||||
return a.activeAt && !b.activeAt ? -1 : 1;
|
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