From 78715bc0984bf7fefadf32b08433e7788a4ad7b4 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sun, 5 May 2019 19:16:02 -0700 Subject: [PATCH] fix: do remote search when autosuggesting accounts (#1182) * fix: do remote search when autosuggesting accounts fixes #1005 * fix emoji search --- .../_actions/autosuggestAccountSearch.js | 79 +++++++++++++++++++ src/routes/_actions/autosuggestEmojiSearch.js | 34 ++++++++ src/routes/_api/search.js | 5 +- src/routes/_database/accounts.js | 14 +--- src/routes/_static/autosuggest.js | 1 + .../_store/observers/autosuggestObservers.js | 43 ++++------ 6 files changed, 132 insertions(+), 44 deletions(-) create mode 100644 src/routes/_actions/autosuggestAccountSearch.js create mode 100644 src/routes/_actions/autosuggestEmojiSearch.js create mode 100644 src/routes/_static/autosuggest.js diff --git a/src/routes/_actions/autosuggestAccountSearch.js b/src/routes/_actions/autosuggestAccountSearch.js new file mode 100644 index 00000000..e843486c --- /dev/null +++ b/src/routes/_actions/autosuggestAccountSearch.js @@ -0,0 +1,79 @@ +import { database } from '../_database/database' +import { store } from '../_store/store' +import { search } from '../_api/search' +import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest' +import { USERNAME_LOWERCASE } from '../_database/constants' +import { concat } from '../_utils/arrays' +import uniqBy from 'lodash-es/uniqBy' +import { scheduleIdleTask } from '../_utils/scheduleIdleTask' + +const DATABASE_SEARCH_RESULTS_LIMIT = 30 + +function byAccountRelevance (a, b) { + // accounts you're following go first + if (a.following !== b.following) { + return a.following ? -1 : 1 + } + // after that, just sort by username + if (a[USERNAME_LOWERCASE] !== b[USERNAME_LOWERCASE]) { + return a[USERNAME_LOWERCASE] < b[USERNAME_LOWERCASE] ? -1 : 1 + } + return 0 +} + +function byAccountId (a) { + return a.id +} + +export function doAccountSearch (searchText) { + let canceled = false + let localResults + let remoteResults + let { currentInstance, accessToken } = store.get() + + async function searchAccountsLocally (searchText) { + localResults = await database.searchAccountsByUsername( + currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT) + } + + async function searchAccountsRemotely (searchText) { + remoteResults = (await search(currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT)).accounts + } + + function mergeAndTruncateResults () { + return uniqBy(concat(localResults || [], remoteResults || []), byAccountId) + .sort(byAccountRelevance) + .slice(0, SEARCH_RESULTS_LIMIT) + } + + function onNewResults () { + if (canceled) { + return + } + let results = mergeAndTruncateResults() + store.setForCurrentAutosuggest({ + autosuggestType: 'account', + autosuggestSelected: 0, + autosuggestSearchResults: results + }) + } + + scheduleIdleTask(() => { + if (canceled) { + return + } + // run the two searches in parallel + searchAccountsLocally(searchText).then(onNewResults).catch(err => { + console.error('could not search locally', err) + }) + searchAccountsRemotely(searchText).then(onNewResults).catch(err => { + console.error('could not search remotely', err) + }) + }) + + return { + cancel: () => { + canceled = true + } + } +} diff --git a/src/routes/_actions/autosuggestEmojiSearch.js b/src/routes/_actions/autosuggestEmojiSearch.js new file mode 100644 index 00000000..d086688e --- /dev/null +++ b/src/routes/_actions/autosuggestEmojiSearch.js @@ -0,0 +1,34 @@ +import { store } from '../_store/store' +import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest' +import { scheduleIdleTask } from '../_utils/scheduleIdleTask' + +function searchEmoji (searchText) { + searchText = searchText.toLowerCase().substring(1) + let { currentCustomEmoji } = store.get() + let results = currentCustomEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText)) + .sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1) + .slice(0, SEARCH_RESULTS_LIMIT) + return results +} + +export function doEmojiSearch (searchText) { + let canceled = false + + scheduleIdleTask(() => { + if (canceled) { + return + } + let results = searchEmoji(searchText) + store.setForCurrentAutosuggest({ + autosuggestType: 'emoji', + autosuggestSelected: 0, + autosuggestSearchResults: results + }) + }) + + return { + cancel: () => { + canceled = true + } + } +} diff --git a/src/routes/_api/search.js b/src/routes/_api/search.js index 5b885122..11298e07 100644 --- a/src/routes/_api/search.js +++ b/src/routes/_api/search.js @@ -1,10 +1,11 @@ import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax' import { auth, basename } from './utils' -export function search (instanceName, accessToken, query) { +export function search (instanceName, accessToken, query, resolve = true, limit = 40) { let url = `${basename(instanceName)}/api/v1/search?` + paramsString({ q: query, - resolve: true + resolve, + limit }) return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT }) } diff --git a/src/routes/_database/accounts.js b/src/routes/_database/accounts.js index 5d8e079a..89b6e62a 100644 --- a/src/routes/_database/accounts.js +++ b/src/routes/_database/accounts.js @@ -18,19 +18,7 @@ export async function searchAccountsByUsername (instanceName, usernamePrefix, li return dbPromise(db, ACCOUNTS_STORE, 'readonly', (accountsStore, callback) => { let keyRange = createAccountUsernamePrefixKeyRange(usernamePrefix.toLowerCase()) accountsStore.index(USERNAME_LOWERCASE).getAll(keyRange, limit).onsuccess = e => { - let results = e.target.result - results = results.sort((a, b) => { - // accounts you're following go first - if (a.following !== b.following) { - return a.following ? -1 : 1 - } - // after that, just sort by username - if (a[USERNAME_LOWERCASE] !== b[USERNAME_LOWERCASE]) { - return a[USERNAME_LOWERCASE] < b[USERNAME_LOWERCASE] ? -1 : 1 - } - return 0 // eslint-disable-line - }) - callback(results) + callback(e.target.result) } }) } diff --git a/src/routes/_static/autosuggest.js b/src/routes/_static/autosuggest.js new file mode 100644 index 00000000..7699d15e --- /dev/null +++ b/src/routes/_static/autosuggest.js @@ -0,0 +1 @@ +export const SEARCH_RESULTS_LIMIT = 4 diff --git a/src/routes/_store/observers/autosuggestObservers.js b/src/routes/_store/observers/autosuggestObservers.js index 19fc01ec..79ff7575 100644 --- a/src/routes/_store/observers/autosuggestObservers.js +++ b/src/routes/_store/observers/autosuggestObservers.js @@ -1,40 +1,25 @@ -import { database } from '../../_database/database' import { store } from '../store' - -const SEARCH_RESULTS_LIMIT = 4 -const DATABASE_SEARCH_RESULTS_LIMIT = 30 - -async function searchAccounts (store, searchText) { - searchText = searchText.substring(1) - let { currentInstance } = store.get() - let results = await database.searchAccountsByUsername( - currentInstance, searchText, DATABASE_SEARCH_RESULTS_LIMIT) - return results.slice(0, SEARCH_RESULTS_LIMIT) -} - -function searchEmoji (store, searchText) { - searchText = searchText.toLowerCase().substring(1) - let { currentCustomEmoji } = store.get() - let results = currentCustomEmoji.filter(emoji => emoji.shortcode.toLowerCase().startsWith(searchText)) - .sort((a, b) => a.shortcode.toLowerCase() < b.shortcode.toLowerCase() ? -1 : 1) - .slice(0, SEARCH_RESULTS_LIMIT) - return results -} +import { doEmojiSearch } from '../../_actions/autosuggestEmojiSearch' +import { doAccountSearch } from '../../_actions/autosuggestAccountSearch' export function autosuggestObservers () { + let lastSearch + store.observe('autosuggestSearchText', async autosuggestSearchText => { let { composeFocused } = store.get() if (!composeFocused || !autosuggestSearchText) { return } let autosuggestType = autosuggestSearchText.startsWith('@') ? 'account' : 'emoji' - let results = (autosuggestType === 'account') - ? await searchAccounts(store, autosuggestSearchText) - : await searchEmoji(store, autosuggestSearchText) - store.setForCurrentAutosuggest({ - autosuggestType, - autosuggestSelected: 0, - autosuggestSearchResults: results - }) + + if (lastSearch) { + lastSearch.cancel() + } + + if (autosuggestType === 'emoji') { + lastSearch = doEmojiSearch(autosuggestSearchText) + } else { + lastSearch = doAccountSearch(autosuggestSearchText) + } }) }