Signal-Desktop/ts/util/filterAndSortConversations.ts

147 lines
3.7 KiB
TypeScript
Raw Normal View History

2021-03-11 21:29:31 +00:00
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import Fuse from 'fuse.js';
2021-03-11 21:29:31 +00:00
import type { ConversationType } from '../state/ducks/conversations';
import { parseAndFormatPhoneNumber } from './libphonenumberInstance';
2021-03-11 21:29:31 +00:00
const FUSE_OPTIONS: Fuse.IFuseOptions<ConversationType> = {
2021-03-11 21:29:31 +00:00
// A small-but-nonzero threshold lets us match parts of E164s better, and makes the
// search a little more forgiving.
threshold: 0.1,
useExtendedSearch: true,
keys: [
2021-04-27 22:35:35 +00:00
{
name: 'searchableTitle',
weight: 1,
},
{
name: 'title',
weight: 1,
},
{
name: 'name',
weight: 1,
},
2021-11-12 01:17:29 +00:00
{
name: 'username',
weight: 1,
},
{
name: 'e164',
weight: 0.5,
},
],
2021-03-11 21:29:31 +00:00
};
const collator = new Intl.Collator();
const cachedIndices = new WeakMap<
ReadonlyArray<ConversationType>,
Fuse<ConversationType>
>();
type CommandRunnerType = (
conversations: ReadonlyArray<ConversationType>,
query: string
) => Array<ConversationType>;
const COMMANDS = new Map<string, CommandRunnerType>();
COMMANDS.set('uuidEndsWith', (conversations, query) => {
return conversations.filter(convo => convo.uuid?.endsWith(query));
});
COMMANDS.set('idEndsWith', (conversations, query) => {
return conversations.filter(convo => convo.id?.endsWith(query));
});
COMMANDS.set('e164EndsWith', (conversations, query) => {
return conversations.filter(convo => convo.e164?.endsWith(query));
});
COMMANDS.set('groupIdEndsWith', (conversations, query) => {
return conversations.filter(convo => convo.groupId?.endsWith(query));
});
// See https://fusejs.io/examples.html#extended-search for
// extended search documentation.
2021-04-28 20:44:48 +00:00
function searchConversations(
conversations: ReadonlyArray<ConversationType>,
searchTerm: string,
regionCode: string | undefined
2021-04-28 20:44:48 +00:00
): Array<ConversationType> {
const maybeCommand = searchTerm.match(/^!([^\s]+):(.*)$/);
if (maybeCommand) {
const [, commandName, query] = maybeCommand;
const command = COMMANDS.get(commandName);
if (command) {
return command(conversations, query);
}
}
const phoneNumber = parseAndFormatPhoneNumber(searchTerm, regionCode);
// Escape the search term
let extendedSearchTerm = searchTerm;
// OR phoneNumber
if (phoneNumber) {
extendedSearchTerm += ` | ${phoneNumber.e164}`;
}
let index = cachedIndices.get(conversations);
if (!index) {
index = new Fuse<ConversationType>(conversations, FUSE_OPTIONS);
cachedIndices.set(conversations, index);
}
const results = index.search(extendedSearchTerm);
return results.map(result => result.item);
2021-04-28 20:44:48 +00:00
}
export function filterAndSortConversationsByRecent(
conversations: ReadonlyArray<ConversationType>,
searchTerm: string,
regionCode: string | undefined
2021-04-28 20:44:48 +00:00
): Array<ConversationType> {
if (searchTerm.length) {
return searchConversations(conversations, searchTerm, regionCode);
2021-04-28 20:44:48 +00:00
}
return conversations.concat().sort((a, b) => {
if (a.activeAt && b.activeAt) {
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
2021-03-11 21:29:31 +00:00
): Array<ConversationType> {
if (searchTerm.length) {
return searchConversations(conversations, searchTerm, regionCode);
2021-03-11 21:29:31 +00:00
}
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);
2021-03-11 21:29:31 +00:00
}