diff --git a/resources/js/components/chat/message-reactions.tsx b/resources/js/components/chat/message-reactions.tsx index e1ab845d4..71e53be27 100644 --- a/resources/js/components/chat/message-reactions.tsx +++ b/resources/js/components/chat/message-reactions.tsx @@ -16,7 +16,7 @@ import { TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; -import { emojiName, searchEmojiByUse } from '@/lib/emoji'; +import { emojiName, searchEmoji } from '@/lib/emoji'; import { formatCount, formatExactCount } from '@/lib/numbers'; import type { ReactionUsage, Tap } from '@/lib/reactions'; import { @@ -529,7 +529,7 @@ export function EmojiPicker({ return [ { name: 'Results', - emoji: searchEmojiByUse(query, uses, 200).map( + emoji: searchEmoji(query, uses).map( (result) => result.emoji, ), }, diff --git a/resources/js/lib/emoji.ts b/resources/js/lib/emoji.ts index 707d690a0..1cc6b1bb4 100644 --- a/resources/js/lib/emoji.ts +++ b/resources/js/lib/emoji.ts @@ -66,33 +66,13 @@ function matchRank(name: string, partial: string): number { } /** - * The shortcodes that start with the query, then the ones with a later word starting with it, then the ones that only - * contain it, each shortest first. + * Each emoji once, under its best matching shortcode: the ones starting with the query, then the ones with a later + * word starting with it, then the ones that only contain it. Within each, the emoji the user uses most come first, + * then the shortest shortcodes. Colons around the query and its case are ignored. */ -export function searchEmoji(query: string, limit = 8): EmojiSuggestion[] { - const partial = query.toLowerCase(); - - return Object.keys(shortcodeEmoji) - .filter((name) => name.includes(partial)) - .map((name) => ({ name, rank: matchRank(name, partial) })) - .sort( - (left, right) => - left.rank - right.rank || - left.name.length - right.name.length || - left.name.localeCompare(right.name), - ) - .slice(0, limit) - .map(({ name }) => ({ name, emoji: shortcodeEmoji[name] })); -} - -/** - * Each emoji once, under its best matching shortcode, ranked as searchEmoji does but with the emoji the user uses - * most first within each kind of match. Colons around the query are ignored. - */ -export function searchEmojiByUse( +export function searchEmoji( query: string, - uses: ReadonlyMap, - limit = 80, + uses: ReadonlyMap = new Map(), ): EmojiSuggestion[] { const partial = query .trim() @@ -124,6 +104,5 @@ export function searchEmojiByUse( left.name.length - right.name.length || left.name.localeCompare(right.name), ) - .slice(0, limit) .map(([emoji, { name }]) => ({ name, emoji })); } diff --git a/tests/Unit/EmojiSearch.test.tsx b/tests/Unit/EmojiSearch.test.tsx index 0e5c8e21f..468f82c2a 100644 --- a/tests/Unit/EmojiSearch.test.tsx +++ b/tests/Unit/EmojiSearch.test.tsx @@ -1,17 +1,15 @@ /// import assert from 'node:assert/strict'; import { test } from 'vite-plus/test'; -import { - emojiName, - searchEmoji, - searchEmojiByUse, -} from '../../resources/js/lib/emoji'; +import { emojiName, searchEmoji } from '../../resources/js/lib/emoji'; test('suggests the shortcodes starting with the query first, shortest first', () => { const names = searchEmoji('smi').map((emoji) => emoji.name); assert.deepEqual(names.slice(0, 3), ['smile', 'smirk', 'smiley']); - assert.ok(names.every((name) => name.startsWith('smi'))); + const prefixed = names.filter((name) => name.startsWith('smi')).length; + + assert.ok(names.slice(0, prefixed).every((name) => name.startsWith('smi'))); }); test('falls back to the shortcodes with a word starting with the query, ignoring case', () => { @@ -28,27 +26,28 @@ test('lists the shortcodes containing the query inside a word last', () => { assert.deepEqual(names, ['apple', 'green_apple', 'pineapple']); }); -test('limits the suggestions and pairs each shortcode with its emoji', () => { - const suggestions = searchEmoji('a', 5); - - assert.equal(suggestions.length, 5); - assert.deepEqual(searchEmoji('+1', 1), [{ name: '+1', emoji: '👍' }]); +test('suggests every matching shortcode and pairs each with its emoji', () => { + assert.equal(searchEmoji('heart').length, 34); + assert.deepEqual( + searchEmoji('boat').filter((result) => result.emoji === '⛵'), + [{ name: 'boat', emoji: '⛵' }], + ); + assert.equal(searchEmoji('ar').length, 253); + assert.deepEqual(searchEmoji('+1'), [{ name: '+1', emoji: '👍' }]); assert.deepEqual(searchEmoji('no_such_shortcode'), []); }); test('searches each emoji once, putting the ones the user reacts with most first within a match', () => { - const results = searchEmojiByUse('heart', new Map([['😍', 3]])); + const results = searchEmoji('heart', new Map([['😍', 3]])); assert.deepEqual( results.slice(0, 2).map((result) => result.emoji), ['😍', '❤️'], ); assert.equal( - searchEmojiByUse('thumbs', new Map()).filter( - (result) => result.emoji === '👍', - ).length, + searchEmoji('thumbs').filter((result) => result.emoji === '👍').length, 1, ); - assert.equal(searchEmojiByUse(':rock', new Map())[0].emoji, '🪨'); + assert.equal(searchEmoji(':rock')[0].emoji, '🪨'); assert.equal(emojiName('👍'), '+1'); });