diff --git a/ts/util/filterAndSortConversations.ts b/ts/util/filterAndSortConversations.ts index b5ecffe9b7..28fb7b893b 100644 --- a/ts/util/filterAndSortConversations.ts +++ b/ts/util/filterAndSortConversations.ts @@ -5,12 +5,19 @@ import Fuse from 'fuse.js'; import type { ConversationType } from '../state/ducks/conversations'; import { parseAndFormatPhoneNumber } from './libphonenumberInstance'; +import { WEEK } from './durations'; + +// Fuse.js scores have order of 0.01 +const ACTIVE_AT_SCORE_FACTOR = (1 / WEEK) * 0.01; const FUSE_OPTIONS: Fuse.IFuseOptions = { // A small-but-nonzero threshold lets us match parts of E164s better, and makes the // search a little more forgiving. threshold: 0.2, + includeScore: true, useExtendedSearch: true, + // We sort manually anyway + shouldSort: true, keys: [ { name: 'searchableTitle', @@ -71,14 +78,14 @@ function searchConversations( conversations: ReadonlyArray, searchTerm: string, regionCode: string | undefined -): Array { +): ReadonlyArray, 'item' | 'score'>> { const maybeCommand = searchTerm.match(/^!([^\s]+):(.*)$/); if (maybeCommand) { const [, commandName, query] = maybeCommand; const command = COMMANDS.get(commandName); if (command) { - return command(conversations, query); + return command(conversations, query).map(item => ({ item })); } } @@ -98,8 +105,7 @@ function searchConversations( cachedIndices.set(conversations, index); } - const results = index.search(extendedSearchTerm); - return results.map(result => result.item); + return index.search(extendedSearchTerm); } export function filterAndSortConversationsByRecent( @@ -108,7 +114,24 @@ export function filterAndSortConversationsByRecent( regionCode: string | undefined ): Array { if (searchTerm.length) { - return searchConversations(conversations, searchTerm, regionCode); + const now = Date.now(); + + return searchConversations(conversations, searchTerm, regionCode) + .slice() + .sort((a, b) => { + const { activeAt: aActiveAt = 0 } = a.item; + const { activeAt: bActiveAt = 0 } = 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); + const bScore = + (now - bActiveAt) * ACTIVE_AT_SCORE_FACTOR + (b.score ?? 0); + + return aScore - bScore; + }) + .map(result => result.item); } return conversations.concat().sort((a, b) => { @@ -126,7 +149,12 @@ export function filterAndSortConversationsByTitle( regionCode: string | undefined ): Array { if (searchTerm.length) { - return searchConversations(conversations, searchTerm, regionCode); + 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) => {