|
11 | 11 | import emojiGroups from '$lib/emoji-groups.json'; |
12 | 12 | import emojiShortCodes from '$lib/emoji-shortcodes.json'; |
13 | 13 |
|
| 14 | + import { settings } from '$lib/stores'; |
| 15 | + import { updateUserSettings } from '$lib/apis/users'; |
| 16 | +
|
14 | 17 | const i18n = getContext('i18n'); |
15 | 18 |
|
16 | 19 | export let onClose = () => {}; |
|
20 | 23 | export let user = null; |
21 | 24 | export let selected = null; |
22 | 25 |
|
| 26 | + const MAX_RECENT = 30; |
| 27 | +
|
23 | 28 | let show = false; |
24 | 29 | let emojis = emojiShortCodes; |
25 | 30 | let search = ''; |
26 | 31 | let flattenedEmojis = []; |
27 | 32 | let emojiRows = []; |
28 | 33 |
|
| 34 | + let saveDebounceTimer: ReturnType<typeof setTimeout> | null = null; |
| 35 | +
|
| 36 | + $: recentEmojiNames = ($settings?.recentEmojis ?? []) |
| 37 | + .filter((name) => emojiShortCodes[name]) |
| 38 | + .slice(0, MAX_RECENT); |
| 39 | +
|
| 40 | + function saveRecentEmoji(emojiName: string) { |
| 41 | + // Remove if already present, then prepend |
| 42 | + const updated = [emojiName, ...recentEmojiNames.filter((n) => n !== emojiName)].slice( |
| 43 | + 0, |
| 44 | + MAX_RECENT |
| 45 | + ); |
| 46 | +
|
| 47 | + // Update store immediately (reactive UI) |
| 48 | + settings.set({ ...$settings, recentEmojis: updated }); |
| 49 | +
|
| 50 | + // Debounce backend save (avoid API spam on rapid picks) |
| 51 | + if (saveDebounceTimer) clearTimeout(saveDebounceTimer); |
| 52 | + saveDebounceTimer = setTimeout(async () => { |
| 53 | + await updateUserSettings(localStorage.token, { ui: { ...$settings, recentEmojis: updated } }); |
| 54 | + }, 1000); |
| 55 | + } |
| 56 | +
|
29 | 57 | // Reactive statement to filter the emojis based on search query |
30 | 58 | $: { |
31 | 59 | if (search) { |
|
55 | 83 | // Flatten emoji groups and group them into rows of 8 for virtual scrolling |
56 | 84 | $: { |
57 | 85 | flattenedEmojis = []; |
| 86 | +
|
| 87 | + // Add "Recently Used" group first (only when not searching) |
| 88 | + if (!search && recentEmojiNames.length > 0) { |
| 89 | + flattenedEmojis.push({ type: 'group', label: $i18n.t('Recently Used') }); |
| 90 | + flattenedEmojis.push( |
| 91 | + ...recentEmojiNames.map((emoji) => ({ |
| 92 | + type: 'emoji', |
| 93 | + name: emoji, |
| 94 | + shortCodes: |
| 95 | + typeof emojiShortCodes[emoji] === 'string' |
| 96 | + ? [emojiShortCodes[emoji]] |
| 97 | + : emojiShortCodes[emoji] |
| 98 | + })) |
| 99 | + ); |
| 100 | + } |
| 101 | +
|
58 | 102 | Object.keys(emojiGroups).forEach((group) => { |
59 | 103 | const groupEmojis = emojiGroups[group].filter((emoji) => emojis[emoji]); |
60 | 104 | if (groupEmojis.length > 0) { |
|
97 | 141 | // Handle emoji selection |
98 | 142 | function selectEmoji(emoji) { |
99 | 143 | const selectedCode = emoji.shortCodes[0]; |
| 144 | + saveRecentEmoji(emoji.name); |
100 | 145 | if (selected === selectedCode) { |
101 | 146 | onSubmit(null); |
102 | 147 | } else { |
|
140 | 185 | {:else} |
141 | 186 | <div class="w-full flex ml-0.5"> |
142 | 187 | <VirtualList rowHeight={ROW_HEIGHT} items={emojiRows} height={384} let:item> |
143 | | - <div class="w-full"> |
| 188 | + <div class="w-full mb-2.5"> |
144 | 189 | {#if item.length === 1 && item[0].type === 'group'} |
145 | 190 | <!-- Render group header --> |
146 | | - <div class="text-xs font-medium mb-2 text-gray-500 dark:text-gray-400"> |
| 191 | + <div class="text-xs font-medium -mb-1 text-gray-500 dark:text-gray-400"> |
147 | 192 | {item[0].label} |
148 | 193 | </div> |
149 | 194 | {:else} |
|
0 commit comments