fix: tweak autocomplete behavior (#1570)
tweak the hashtag sort algorithm fix issue where wrong results shown when offline or on slow network refactor RequestThrottler
This commit is contained in:
parent
89265f709e
commit
3209d934e8
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue