diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index ab115ab562..68e3ead65f 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -19,6 +19,8 @@ import { updateTimeline } from './timelines'; let fetchComposeSuggestionsAccountsController; /** @type {AbortController | undefined} */ let fetchComposeSuggestionsTagsController; +/** @type {AbortController | undefined} */ +let searchComposeSuggestionsEmojiController; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; @@ -499,9 +501,8 @@ export function undoUploadCompose(media_id) { } export function clearComposeSuggestions() { - if (fetchComposeSuggestionsAccountsController) { - fetchComposeSuggestionsAccountsController.abort(); - } + fetchComposeSuggestionsAccountsController?.abort(); + searchComposeSuggestionsEmojiController?.abort(); return { type: COMPOSE_SUGGESTIONS_CLEAR, }; @@ -534,12 +535,25 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, token) => { }); }, 200, { leading: true, trailing: true }); -const fetchComposeSuggestionsEmojis = async (dispatch, token) => { - // Right now we are hard-coding the locale to English since the picker search only supports English. - // Once we replace the legacy picker we can remove this and use the actual locale of the user. - const results = await emojiMartSearch(token, 'en', 5); - dispatch(readyComposeSuggestionsEmojis(token, results)); -}; +const fetchComposeSuggestionsEmojis = (dispatch, token) => { + dispatch(clearComposeSuggestions()); + searchComposeSuggestionsEmojiController = new AbortController(); + + void emojiMartSearch({ + token, + // Right now we are hard-coding the locale to English since the picker search only supports English. + // Once we replace the legacy picker we can remove this and use the actual locale of the user. + locale: 'en', + limit: 5, + signal: searchComposeSuggestionsEmojiController.signal, + }).then((results) => { + if (results) { + dispatch(readyComposeSuggestionsEmojis(token, results)); + } + }).finally(() => { + searchComposeSuggestionsEmojiController = undefined; + }); +} const fetchComposeSuggestionsTags = throttle((dispatch, token) => { if (fetchComposeSuggestionsTagsController) { diff --git a/app/javascript/mastodon/features/emoji/picker.ts b/app/javascript/mastodon/features/emoji/picker.ts index c83df7ba8c..f638c91907 100644 --- a/app/javascript/mastodon/features/emoji/picker.ts +++ b/app/javascript/mastodon/features/emoji/picker.ts @@ -22,34 +22,50 @@ type LegacyEmoji = }; // Replicates the old legacy search function. -export async function emojiMartSearch( - token: string, - locale: string, +export async function emojiMartSearch({ + token, + locale, limit = 5, -): Promise { - const query = token.replace(':', '').trim(); - if (!query.length) { - return []; + signal, +}: { + token: string; + locale: string; + limit?: number; + signal?: AbortSignal; +}): Promise { + try { + const query = token.replace(':', '').trim(); + if (!query.length) { + return []; + } + + const cacheKey = `${query}|${locale}|${limit}`; + const cachedResult = searchCache.get(cacheKey); + if (cachedResult) { + return cachedResult; + } + + const results = await search({ + query, + locale, + limit, + signal, + }); + const legacyResults = results.map((emoji) => + 'shortcode' in emoji + ? ({ id: emoji.shortcode, custom: true } as const) + : { + id: emoji.label.replaceAll(' ', '_').toLowerCase(), + native: emoji.unicode, + }, + ); + searchCache.set(cacheKey, legacyResults); + + return legacyResults; + } catch { + log('aborted search for "%s"', token); + return null; } - - const cacheKey = `${query}|${locale}|${limit}`; - const cachedResult = searchCache.get(cacheKey); - if (cachedResult) { - return cachedResult; - } - - const results = await search({ query, locale, limit }); - const legacyResults = results.map((emoji) => - 'shortcode' in emoji - ? ({ id: emoji.shortcode, custom: true } as const) - : { - id: emoji.label.replaceAll(' ', '_').toLowerCase(), - native: emoji.unicode, - }, - ); - searchCache.set(cacheKey, legacyResults); - - return legacyResults; } const defaultCategories = [ diff --git a/app/javascript/mastodon/features/emoji/search.ts b/app/javascript/mastodon/features/emoji/search.ts index dcdd6be500..140cd4b205 100644 --- a/app/javascript/mastodon/features/emoji/search.ts +++ b/app/javascript/mastodon/features/emoji/search.ts @@ -56,13 +56,18 @@ export async function search({ query: rawQuery, locale: localeString, limit = 0, + signal, }: { query: string; locale: string; limit?: number; + signal?: AbortSignal; }) { + log('searching for "%s"', rawQuery); performance.mark('emoji-search-start'); + signal?.throwIfAborted(); + // Get the locale, and extract tokens from the query. const locale = toSupportedLocale(localeString); const segmenter = localeToSegmenter(locale); @@ -93,6 +98,7 @@ export async function search({ locale, i === queryTokens.length - 1, ); + signal?.throwIfAborted(); const resultMap: ScoreMap = new Map(); const checkedSet = new Set(); @@ -121,6 +127,7 @@ export async function search({ } // Score based on legacy shortcodes, using the higher score if there's a match. + signal?.throwIfAborted(); for (const shortcodeResult of shortcodeResults) { const emoji = resultMap.get(shortcodeResult.hexcode) ?? @@ -183,7 +190,9 @@ export async function search({ // If there are no results, try a cursor-based custom emoji search instead. if (mixedResults.length === 0 || mixedResults.length < limit) { + signal?.throwIfAborted(); const customEmojisFound = await fullCustomSearch(query, allEmojiIds); + signal?.throwIfAborted(); if (customEmojisFound.length > 0) { log( 'cursor search found %d results for "%s"', @@ -373,6 +382,8 @@ async function fullCustomSearch(query: string, existing = new Set()) { // First iterate over chunks of 1,000 custom emoji keys and find any matches. const chunkSize = 1_000; + const maxIterations = 10; + let index = 0; let lastKey: string | null = null; let keys: string[] = []; do { @@ -389,6 +400,10 @@ async function fullCustomSearch(query: string, existing = new Set()) { foundEmojis.add(key); } } + index++; + if (index >= maxIterations) { + break; + } } while (keys.length === chunkSize); // Next get the full emojis for all matches. diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 98888c5915..b37d863e92 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -518,7 +518,7 @@ body > [data-popper-placement] { } .autosuggest-account .account__avatar, -.autosuggest-emoji img { +.autosuggest-emoji .emojione { display: block; width: 24px; height: 24px;