From 0613e0ac84a3f2b75d8540fb0de7120e33c96f80 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Jan 2026 19:14:01 +0000 Subject: [PATCH] Improve emoji picker UX with fixed layout - Always show exactly 2 rows (16 emoji) to prevent height jumping - Merge recently used with search results into unified grid - When no search: show recently used first, then fill with other emoji - When searching: show top 16 results - Remove separate "Recently used" section for cleaner layout - Add aspect-square to buttons for consistent sizing - Add object-contain to custom emoji for proper aspect ratio - Replace scrollable area with fixed-height grid --- src/components/chat/EmojiPickerDialog.tsx | 160 +++++++++------------- 1 file changed, 67 insertions(+), 93 deletions(-) diff --git a/src/components/chat/EmojiPickerDialog.tsx b/src/components/chat/EmojiPickerDialog.tsx index 149ab60..f1693c5 100644 --- a/src/components/chat/EmojiPickerDialog.tsx +++ b/src/components/chat/EmojiPickerDialog.tsx @@ -73,8 +73,8 @@ export function EmojiPickerDialog({ // Perform search when query changes useEffect(() => { const performSearch = async () => { - // Use higher limit for dialog vs autocomplete (48 vs 24) - const results = await service.search(searchQuery, { limit: 48 }); + // Always fetch 16 emoji (2 rows of 8) for consistent height + const results = await service.search(searchQuery, { limit: 16 }); setSearchResults(results); }; performSearch(); @@ -82,14 +82,55 @@ export function EmojiPickerDialog({ // Get frequently used emojis from history const frequentlyUsed = useMemo(() => { - if (searchQuery.trim()) return []; // Only show when no search query - const history = getReactionHistory(); return Object.entries(history) .sort((a, b) => b[1] - a[1]) // Sort by count descending - .slice(0, 8) + .slice(0, 16) // Max 2 rows .map(([emoji]) => emoji); - }, [searchQuery]); + }, []); + + // Combine recently used with search results for display + // When no search query: show recently used first, then fill with other emoji + // When searching: show search results + const displayEmojis = useMemo(() => { + if (searchQuery.trim()) { + // Show search results + return searchResults; + } + + // No search query: prioritize recently used, then fill with other emoji + if (frequentlyUsed.length > 0) { + const recentSet = new Set(frequentlyUsed); + // Get additional emoji to fill to 16, excluding recently used + const additional = searchResults + .filter((r) => { + const key = r.source === "unicode" ? r.url : `:${r.shortcode}:`; + return !recentSet.has(key); + }) + .slice(0, 16 - frequentlyUsed.length); + + // Combine: recently used get priority, but displayed as regular emoji + const recentResults: EmojiSearchResult[] = []; + for (const emojiStr of frequentlyUsed) { + if (emojiStr.startsWith(":") && emojiStr.endsWith(":")) { + const shortcode = emojiStr.slice(1, -1); + const customEmoji = service.getByShortcode(shortcode); + if (customEmoji) { + recentResults.push(customEmoji); + } + } else { + // Unicode emoji - find it in search results + const found = searchResults.find((r) => r.url === emojiStr); + if (found) recentResults.push(found); + } + } + + return [...recentResults, ...additional].slice(0, 16); + } + + // No history: just show top 16 emoji + return searchResults; + }, [searchQuery, searchResults, frequentlyUsed, service]); const handleEmojiClick = (result: EmojiSearchResult) => { if (result.source === "unicode") { @@ -108,44 +149,6 @@ export function EmojiPickerDialog({ setSearchQuery(""); // Reset search on close }; - // Helper to render a frequently used emoji (handles both unicode and custom) - const renderFrequentEmoji = (emojiStr: string) => { - // Check if it's a custom emoji shortcode (e.g., ":yesno:") - if (emojiStr.startsWith(":") && emojiStr.endsWith(":")) { - const shortcode = emojiStr.slice(1, -1); - // Look up the emoji in the service - const customEmoji = service.getByShortcode(shortcode); - if (customEmoji && customEmoji.url) { - return {emojiStr}; - } - // Fallback to text if not found - return {emojiStr}; - } - // Unicode emoji - render as text - return {emojiStr}; - }; - - const handleFrequentEmojiClick = (emojiStr: string) => { - // Check if it's a custom emoji shortcode - if (emojiStr.startsWith(":") && emojiStr.endsWith(":")) { - const shortcode = emojiStr.slice(1, -1); - const customEmoji = service.getByShortcode(shortcode); - if (customEmoji && customEmoji.url) { - onEmojiSelect(emojiStr, { - shortcode: shortcode, - url: customEmoji.url, - }); - } else { - // Fallback to treating as unicode - onEmojiSelect(emojiStr); - } - } else { - // Unicode emoji - onEmojiSelect(emojiStr); - } - onOpenChange(false); - }; - return ( @@ -162,55 +165,26 @@ export function EmojiPickerDialog({ /> - {/* Frequently used section */} - {frequentlyUsed.length > 0 && ( -
-
- Recently used -
-
- {frequentlyUsed.map((emoji) => ( - - ))} -
-
- )} - - {/* Emoji grid */} -
- {searchResults.length > 0 ? ( -
- {searchResults.map((result) => ( - - ))} -
- ) : ( -
- No emojis found -
- )} + {/* Fixed 2-row emoji grid (16 emoji) */} +
+ {displayEmojis.map((result) => ( + + ))}