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 localResults
|
||||||
let remoteResults
|
let remoteResults
|
||||||
const { currentInstance, accessToken } = store.get()
|
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(
|
localResults = await database.searchAccountsByUsername(
|
||||||
currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT)
|
currentInstance, searchText.substring(1), DATABASE_SEARCH_RESULTS_LIMIT)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function searchAccountsRemotely (signal) {
|
async function searchAccountsRemotely () {
|
||||||
return (await search(
|
remoteResults = await requestThrottler.request()
|
||||||
currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, signal
|
|
||||||
)).accounts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNewRemoteResults (results) {
|
async function doSearchAccountsRemotely (signal) {
|
||||||
remoteResults = results
|
return (await search(
|
||||||
onNewResults()
|
currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, false, signal
|
||||||
|
)).accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeAndTruncateResults () {
|
function mergeAndTruncateResults () {
|
||||||
|
@ -79,8 +78,8 @@ export function doAccountSearch (searchText) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// run the two searches in parallel
|
// run the two searches in parallel
|
||||||
searchAccountsLocally(searchText).then(onNewResults)
|
searchAccountsLocally().then(onNewResults)
|
||||||
requestThrottler.request()
|
searchAccountsRemotely().then(onNewResults)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -3,27 +3,42 @@ import { store } from '../_store/store'
|
||||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
import { SEARCH_RESULTS_LIMIT } from '../_static/autosuggest'
|
||||||
import { RequestThrottler } from '../_utils/RequestThrottler'
|
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) {
|
export function doHashtagSearch (searchText) {
|
||||||
const { currentInstance, accessToken } = store.get()
|
const { currentInstance, accessToken } = store.get()
|
||||||
const requestThrottler = new RequestThrottler(searchHashtagsRemotely, onNewResults)
|
const requestThrottler = new RequestThrottler(searchHashtags)
|
||||||
|
|
||||||
async function searchHashtagsRemotely (signal) {
|
async function searchHashtags (signal) {
|
||||||
return (await search(
|
const results = await search(
|
||||||
currentInstance, accessToken, searchText, false, SEARCH_RESULTS_LIMIT, signal
|
currentInstance, accessToken, searchText, false, HASHTAG_SEARCH_LIMIT, true, signal
|
||||||
)).hashtags
|
)
|
||||||
|
return results.hashtags.sort(byUsesThenName).slice(0, SEARCH_RESULTS_LIMIT)
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNewResults (results) {
|
scheduleIdleTask(async () => {
|
||||||
|
const results = await requestThrottler.request()
|
||||||
store.setForCurrentAutosuggest({
|
store.setForCurrentAutosuggest({
|
||||||
autosuggestType: 'hashtag',
|
autosuggestType: 'hashtag',
|
||||||
autosuggestSelected: 0,
|
autosuggestSelected: 0,
|
||||||
autosuggestSearchResults: results
|
autosuggestSearchResults: results
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
scheduleIdleTask(() => {
|
|
||||||
requestThrottler.request()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
import { get, paramsString, DEFAULT_TIMEOUT } from '../_utils/ajax'
|
||||||
import { auth, basename } from './utils'
|
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({
|
const url = `${basename(instanceName)}/api/${version}/search?` + paramsString({
|
||||||
q: query,
|
q: query,
|
||||||
resolve,
|
resolve,
|
||||||
limit
|
limit,
|
||||||
|
exclude_unreviewed: !!excludeUnreviewed
|
||||||
})
|
})
|
||||||
return get(url, auth(accessToken), {
|
return get(url, auth(accessToken), {
|
||||||
timeout: DEFAULT_TIMEOUT,
|
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) {
|
async function doSearchV1 (instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal) {
|
||||||
const resp = await doSearch('v1', instanceName, accessToken, query, resolve, limit, signal)
|
const resp = await doSearch('v1', instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal)
|
||||||
resp.hashtags = resp.hashtags && resp.hashtags.map(tag => ({
|
resp.hashtags = resp.hashtags && resp.hashtags.map(tag => ({
|
||||||
name: tag,
|
name: tag,
|
||||||
url: `${basename(instanceName)}/tags/${tag.toLowerCase()}`,
|
url: `${basename(instanceName)}/tags/${tag.toLowerCase()}`,
|
||||||
|
@ -23,16 +24,17 @@ async function doSearchV1 (instanceName, accessToken, query, resolve, limit, sig
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doSearchV2 (instanceName, accessToken, query, resolve, limit, signal) {
|
async function doSearchV2 (instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal) {
|
||||||
return doSearch('v2', instanceName, accessToken, query, resolve, limit, 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 {
|
try {
|
||||||
return (await doSearchV2(instanceName, accessToken, query, resolve, limit, signal))
|
return (await doSearchV2(instanceName, accessToken, query, resolve, limit, excludeUnreviewed, signal))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err && err.status === 404) { // fall back to old search API
|
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 {
|
} else {
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,13 @@ import { doEmojiSearch } from '../../_actions/autosuggestEmojiSearch'
|
||||||
import { doAccountSearch } from '../../_actions/autosuggestAccountSearch'
|
import { doAccountSearch } from '../../_actions/autosuggestAccountSearch'
|
||||||
import { doHashtagSearch } from '../../_actions/autosuggestHashtagSearch'
|
import { doHashtagSearch } from '../../_actions/autosuggestHashtagSearch'
|
||||||
|
|
||||||
|
function resetAutosuggest () {
|
||||||
|
store.setForCurrentAutosuggest({
|
||||||
|
autosuggestSelected: 0,
|
||||||
|
autosuggestSearchResults: []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function autosuggestObservers () {
|
export function autosuggestObservers () {
|
||||||
let lastSearch
|
let lastSearch
|
||||||
|
|
||||||
|
@ -20,6 +27,7 @@ export function autosuggestObservers () {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetAutosuggest()
|
||||||
if (autosuggestSearchText.startsWith(':')) { // emoji
|
if (autosuggestSearchText.startsWith(':')) { // emoji
|
||||||
lastSearch = doEmojiSearch(autosuggestSearchText)
|
lastSearch = doEmojiSearch(autosuggestSearchText)
|
||||||
} else if (autosuggestSearchText.startsWith('#')) { // hashtag
|
} else if (autosuggestSearchText.startsWith('#')) { // hashtag
|
||||||
|
|
|
@ -4,24 +4,22 @@ import { PromiseThrottler } from './PromiseThrottler'
|
||||||
const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms
|
const promiseThrottler = new PromiseThrottler(200) // Mastodon FE also uses 200ms
|
||||||
|
|
||||||
export class RequestThrottler {
|
export class RequestThrottler {
|
||||||
constructor (fetcher, onNewResults) {
|
constructor (fetcher) {
|
||||||
this._canceled = false
|
this._canceled = false
|
||||||
this._controller = typeof AbortController === 'function' && new AbortController()
|
this._controller = typeof AbortController === 'function' && new AbortController()
|
||||||
this._fetcher = fetcher
|
this._fetcher = fetcher
|
||||||
this._onNewResults = onNewResults
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async request () {
|
async request () {
|
||||||
if (this._canceled) {
|
if (this._canceled) {
|
||||||
return
|
throw new Error('canceled')
|
||||||
}
|
}
|
||||||
await promiseThrottler.next()
|
await promiseThrottler.next()
|
||||||
if (this._canceled) {
|
if (this._canceled) {
|
||||||
return
|
throw new Error('canceled')
|
||||||
}
|
}
|
||||||
const signal = this._controller && this._controller.signal
|
const signal = this._controller && this._controller.signal
|
||||||
const results = await this._fetcher(signal)
|
return this._fetcher(signal)
|
||||||
this._onNewResults(results)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel () {
|
cancel () {
|
||||||
|
|
|
@ -28,3 +28,11 @@ export function padStart (string, length, chars) {
|
||||||
}
|
}
|
||||||
return string
|
return string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sum (list) {
|
||||||
|
let total = 0
|
||||||
|
for (const item of list) {
|
||||||
|
total += item
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue