diff --git a/src/routes/_actions/autosuggestAccountSearch.js b/src/routes/_actions/autosuggestAccountSearch.js index 783c8f76..c00f4df6 100644 --- a/src/routes/_actions/autosuggestAccountSearch.js +++ b/src/routes/_actions/autosuggestAccountSearch.js @@ -25,22 +25,21 @@ export function doAccountSearch (searchText) { let localResults let remoteResults const { currentInstance, accessToken } = store.get() - const requestThrottler = new RequestThrottler(searchAccountsRemotely, onNewRemoteResults) + const requestThrottler = new RequestThrottler(doSearchAccountsRemotely) - async function searchAccountsLocally (searchText) { + async function searchAccountsLocally () { localResults = await database.searchAccountsByUsername( currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT) } - async function searchAccountsRemotely (signal) { - return (await search( - currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, signal - )).accounts + async function searchAccountsRemotely () { + remoteResults = await requestThrottler.request() } - function onNewRemoteResults (results) { - remoteResults = results - onNewResults() + async function doSearchAccountsRemotely (signal) { + return (await search( + currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, false, signal + )).accounts } function mergeAndTruncateResults () { @@ -79,8 +78,8 @@ export function doAccountSearch (searchText) { return } // run the two searches in parallel - searchAccountsLocally(searchText).then(onNewResults) - requestThrottler.request() + searchAccountsLocally().then(onNewResults) + searchAccountsRemotely().then(onNewResults) }) return { diff --git a/src/routes/_actions/autosuggestHashtagSearch.js b/src/routes/_actions/autosuggestHashtagSearch.js index bd12b15c..10f1fd08 100644 --- a/src/routes/_actions/autosuggestHashtagSearch.js +++ b/src/routes/_actions/autosuggestHashtagSearch.js @@ -3,27 +3,42 @@ import { store } from '../_store/store' import { scheduleIdleTask } from '../_utils/scheduleIdleTask' import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest' import { RequestThrottler } from '../_utils/RequestThrottler' +import { sum } from '../_utils/lodash-lite' + +const HASHTAG_SEARCH_LIMIT = 10 + +function getUses (historyItem) { + return historyItem.uses +} + +// Show the most common hashtags first, then sort by name +function byUsesThenName (a, b) { + if (a.history && b.history && a.history.length && b.history.length) { + const aCount = sum(a.history.map(getUses)) + const bCount = sum(b.history.map(getUses)) + return aCount > bCount ? -1 : aCount < bCount ? 1 : 0 + } + return a.name < b.name ? -1 : a.name > b.name ? 1 : 0 +} export function doHashtagSearch (searchText) { const { currentInstance, accessToken } = store.get() - const requestThrottler = new RequestThrottler(searchHashtagsRemotely, onNewResults) + const requestThrottler = new RequestThrottler(searchHashtags) - async function searchHashtagsRemotely (signal) { - return (await search( - currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, signal - )).hashtags + async function searchHashtags (signal) { + const results = await search( + currentInstance, accessToken, searchText, false, HASHTAG_SEARCH_LIMIT, true, signal + ) + return results.hashtags.sort(byUsesThenName).slice(0, SEARCH_RESULTS_LIMIT) } - function onNewResults (results) { + scheduleIdleTask(async () => { + const results = await requestThrottler.request() store.setForCurrentAutosuggest({ autosuggestType: 'hashtag', autosuggestSelected: 0, autosuggestSearchResults: results }) - } - - scheduleIdleTask(() => { - requestThrottler.request() }) return { diff --git a/src/routes/_api/search.js b/src/routes/_api/search.js index 290f71b5..4e8e96cc 100644 --- a/src/routes/_api/search.js +++ b/src/routes/_api/search.js @@ -1,11 +1,12 @@ import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax' import { auth, basename } from './utils' -function doSearch (version, instanceName, accessToken, query, resolve, limit, signal) { +function doSearch (version, instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal) { const url = `${basename(instanceName)}/api/${version}/search?` + paramsString({ q: query, resolve, - limit + limit, + exclude_unreviewed: !!excludeUnreviewed }) return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT, @@ -13,8 +14,8 @@ function doSearch (version, instanceName, accessToken, query, resolve, limit, si }) } -async function doSearchV1 (instanceName, accessToken, query, resolve, limit, signal) { - const resp = await doSearch('v1', instanceName, accessToken, query, resolve, limit, signal) +async function doSearchV1 (instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal) { + const resp = await doSearch('v1', instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal) resp.hashtags = resp.hashtags && resp.hashtags.map(tag => ({ name: tag, url: `${basename(instanceName)}/tags/${tag.toLowerCase()}`, @@ -23,16 +24,17 @@ async function doSearchV1 (instanceName, accessToken, query, resolve, limit, sig return resp } -async function doSearchV2 (instanceName, accessToken, query, resolve, limit, signal) { - return doSearch('v2', instanceName, accessToken, query, resolve, limit, signal) +async function doSearchV2 (instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal) { + return doSearch('v2', instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal) } -export async function search (instanceName, accessToken, query, resolve = true, limit = 5, signal = null) { +export async function search (instanceName, accessToken, query, resolve = true, limit = 5, + excludeUnreviewed = false, signal = null) { try { - return (await doSearchV2(instanceName, accessToken, query, resolve, limit, signal)) + return (await doSearchV2(instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal)) } catch (err) { if (err && err.status === 404) { // fall back to old search API - return doSearchV1(instanceName, accessToken, query, resolve, limit, signal) + return doSearchV1(instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal) } else { throw err } diff --git a/src/routes/_store/observers/autosuggestObservers.js b/src/routes/_store/observers/autosuggestObservers.js index d61fe9d6..1175c264 100644 --- a/src/routes/_store/observers/autosuggestObservers.js +++ b/src/routes/_store/observers/autosuggestObservers.js @@ -3,6 +3,13 @@ import { doEmojiSearch } from '../../_actions/autosuggestEmojiSearch' import { doAccountSearch } from '../../_actions/autosuggestAccountSearch' import { doHashtagSearch } from '../../_actions/autosuggestHashtagSearch' +function resetAutosuggest () { + store.setForCurrentAutosuggest({ + autosuggestSelected: 0, + autosuggestSearchResults: [] + }) +} + export function autosuggestObservers () { let lastSearch @@ -20,6 +27,7 @@ export function autosuggestObservers () { return } + resetAutosuggest() if (autosuggestSearchText.startsWith(':')) { // emoji lastSearch = doEmojiSearch(autosuggestSearchText) } else if (autosuggestSearchText.startsWith('#')) { // hashtag diff --git a/src/routes/_utils/RequestThrottler.js b/src/routes/_utils/RequestThrottler.js index 908ca132..7c90df80 100644 --- a/src/routes/_utils/RequestThrottler.js +++ b/src/routes/_utils/RequestThrottler.js @@ -4,24 +4,22 @@ import { PromiseThrottler } from './PromiseThrottler' const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms export class RequestThrottler { - constructor (fetcher, onNewResults) { + constructor (fetcher) { this._canceled = false this._controller = typeof AbortController === 'function' && new AbortController() this._fetcher = fetcher - this._onNewResults = onNewResults } async request () { if (this._canceled) { - return + throw new Error('canceled') } await promiseThrottler.next() if (this._canceled) { - return + throw new Error('canceled') } const signal = this._controller && this._controller.signal - const results = await this._fetcher(signal) - this._onNewResults(results) + return this._fetcher(signal) } cancel () { diff --git a/src/routes/_utils/lodash-lite.js b/src/routes/_utils/lodash-lite.js index 0e0fe4fd..99d30577 100644 --- a/src/routes/_utils/lodash-lite.js +++ b/src/routes/_utils/lodash-lite.js @@ -28,3 +28,11 @@ export function padStart (string, length, chars) { } return string } + +export function sum (list) { + let total = 0 + for (const item of list) { + total += item + } + return total +}