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:
Nolan Lawson 2019-10-13 08:08:06 -07:00 committed by GitHub
parent 89265f709e
commit 3209d934e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 66 additions and 36 deletions

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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

View file

@ -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 () {

View file

@ -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
}