From fd0b3863f062909becd6f7eeba85c9bdfd28f7ec Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Sun, 16 Aug 2026 19:25:50 -0600 Subject: [PATCH] Add a "Recently used" section to the emoji picker (#35) Shows up to 8 most-recently-picked emoji, pinned above the regular categories, whenever the picker isn't in search mode -- shared by both the composer's insert-emoji button and message reactions, since both go through the same EmojiPicker component. Stored in localStorage (frontend/src/lib/recentEmoji.ts), per-browser rather than synced across devices, matching this app's existing local-only preferences (e.g. the resizable-panel widths). Verified in-browser: no section when empty, a pick is recorded and shows up on reopen, order is most-recent-first, re-picking an already- recent emoji moves it to the front without duplicating, and the list caps at 8 by dropping the oldest entry. Co-Authored-By: Claude Sonnet 5 --- frontend/src/components/EmojiPicker.tsx | 70 ++++++++++++++++++------- frontend/src/lib/recentEmoji.ts | 26 +++++++++ 2 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 frontend/src/lib/recentEmoji.ts diff --git a/frontend/src/components/EmojiPicker.tsx b/frontend/src/components/EmojiPicker.tsx index 828b43d..6ec692b 100644 --- a/frontend/src/components/EmojiPicker.tsx +++ b/frontend/src/components/EmojiPicker.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react' import { useEscapeKey } from '../hooks/useEscapeKey' import { ALL_EMOJI, EMOJI_CATEGORIES } from '../lib/emoji' import { EMOJI_NAMES } from '../lib/emojiNames' +import { getRecentEmoji, recordEmojiUsed } from '../lib/recentEmoji' import './EmojiPicker.css' interface EmojiPickerProps { @@ -39,6 +40,16 @@ export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'lef const [query, setQuery] = useState('') const searchResults = useMemo(() => searchEmoji(query), [query]) const searching = query.trim().length > 0 + // A snapshot taken once when the picker opens, not live-updating as picks + // happen within this same session -- picking an emoji always closes the + // picker (see Composer.tsx/MessageList.tsx), so there's never a second + // pick in the same open session to show an updated list to. + const [recent] = useState(getRecentEmoji) + + function pick(emoji: string) { + recordEmojiUsed(emoji) + onPick(emoji) + } return ( <> @@ -65,7 +76,7 @@ export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'lef role="menuitem" className="emoji-picker-item" title={EMOJI_NAMES[emoji]?.name} - onClick={() => onPick(emoji)} + onClick={() => pick(emoji)} > {emoji} @@ -75,25 +86,46 @@ export function EmojiPicker({ onPick, onClose, placement = 'below', align = 'lef
No emoji found
) ) : ( - EMOJI_CATEGORIES.map((category) => ( -
-
{category.label}
-
- {category.emoji.map((emoji) => ( - - ))} + <> + {recent.length > 0 && ( +
+
Recently used
+
+ {recent.map((emoji) => ( + + ))} +
-
- )) + )} + {EMOJI_CATEGORIES.map((category) => ( +
+
{category.label}
+
+ {category.emoji.map((emoji) => ( + + ))} +
+
+ ))} + )}
diff --git a/frontend/src/lib/recentEmoji.ts b/frontend/src/lib/recentEmoji.ts new file mode 100644 index 0000000..00794e1 --- /dev/null +++ b/frontend/src/lib/recentEmoji.ts @@ -0,0 +1,26 @@ +const KEY = 'recent-emoji' +const MAX_RECENT = 8 + +export function getRecentEmoji(): string[] { + try { + const raw = localStorage.getItem(KEY) + if (!raw) return [] + const parsed = JSON.parse(raw) + return Array.isArray(parsed) ? parsed.filter((e) => typeof e === 'string') : [] + } catch { + return [] + } +} + +// Most-recently-used first, deduped, capped -- a picked emoji that's +// already in the list moves to the front rather than appearing twice. +export function recordEmojiUsed(emoji: string): void { + try { + const current = getRecentEmoji().filter((e) => e !== emoji) + const next = [emoji, ...current].slice(0, MAX_RECENT) + localStorage.setItem(KEY, JSON.stringify(next)) + } catch { + // storage unavailable (private browsing, quota) -- recent emoji just + // won't persist this session, not fatal. + } +}