import { TELEGRAM_EMOJI_GROUPS, getTelegramEmojiAssetUrl, getTelegramEmojiPreviewUrl, } from './telegram-emoji-catalog.js?v=202607151545'; const RECENT_STORAGE_KEY = 'shine-ui-recent-emojis-v1'; const MAX_RECENT = 28; const ALL_EMOJI_ITEMS = TELEGRAM_EMOJI_GROUPS.flatMap((group) => group.items); const EMOJI_ASSET_BY_CHAR = new Map(ALL_EMOJI_ITEMS.map((item) => [item.emoji, item])); const TELEGRAM_EMOJI_MATCHER = new RegExp( [...EMOJI_ASSET_BY_CHAR.keys()] .sort((left, right) => right.length - left.length) .map((emoji) => emoji.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')) .join('|'), 'gu', ); const DEFAULT_RECENT = ALL_EMOJI_ITEMS.slice(0, 7).map((item) => item.emoji); let activeAnimation = null; function loadRecent() { try { const parsed = JSON.parse(localStorage.getItem(RECENT_STORAGE_KEY) || '[]'); return Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string' && item).slice(0, MAX_RECENT) : []; } catch { return []; } } function saveRecent(items) { try { localStorage.setItem(RECENT_STORAGE_KEY, JSON.stringify(items.slice(0, MAX_RECENT))); } catch { // Selection remains usable when browser storage is unavailable. } } function appendAnimatedAsset(root, item) { const previewUrl = item.previewUrl || getTelegramEmojiPreviewUrl(item.file); const animatedUrl = item.animatedUrl || getTelegramEmojiAssetUrl(item.file); const image = document.createElement('img'); image.className = 'telegram-emoji-image'; image.alt = ''; image.decoding = 'async'; image.setAttribute('aria-hidden', 'true'); let isActive = false; let sourceVersion = 0; const setSource = (source) => { const requestedVersion = sourceVersion + 1; sourceVersion = requestedVersion; root.classList.remove('is-error'); image.onload = () => { if (requestedVersion !== sourceVersion) return; if (isActive) root.classList.remove('is-loading'); }; image.onerror = () => { if (requestedVersion !== sourceVersion) return; if (isActive) { isActive = false; root.classList.remove('is-active', 'is-loading'); if (activeAnimation?.root === root) activeAnimation = null; setSource(previewUrl); return; } root.classList.add('is-error'); image.removeAttribute('src'); }; image.src = source; }; const deactivate = () => { isActive = false; root.classList.remove('is-active', 'is-loading', 'is-error'); if (activeAnimation?.root === root) activeAnimation = null; setSource(previewUrl); }; const activate = () => { if (activeAnimation?.root && activeAnimation.root !== root) { activeAnimation.deactivate(); } activeAnimation = { root, deactivate }; isActive = true; root.classList.add('is-active', 'is-loading'); setSource(animatedUrl); }; const onPointerOver = (event) => { if (event.relatedTarget && root.contains(event.relatedTarget)) return; if (event.pointerType === 'mouse') activate(); }; const onPointerOut = (event) => { if (event.relatedTarget && root.contains(event.relatedTarget)) return; if (event.pointerType === 'mouse') deactivate(); }; const onPointerDown = (event) => { if (event.pointerType === 'mouse') return; root.setPointerCapture?.(event.pointerId); activate(); }; const onPointerEnd = (event) => { if (event.pointerType === 'mouse') return; if (root.hasPointerCapture?.(event.pointerId)) root.releasePointerCapture?.(event.pointerId); deactivate(); }; root.addEventListener('pointerover', onPointerOver); root.addEventListener('pointerout', onPointerOut); root.addEventListener('pointerdown', onPointerDown); root.addEventListener('pointerup', onPointerEnd); root.addEventListener('pointercancel', onPointerEnd); root.addEventListener('lostpointercapture', deactivate); setSource(previewUrl); root.append(image); } export function stopAllTelegramEmojiAnimations() { activeAnimation?.deactivate(); activeAnimation = null; } function createEmojiButton(item, onSelect) { const button = document.createElement('button'); button.type = 'button'; button.className = 'emoji-picker-item'; button.setAttribute('aria-label', `Add ${item.emoji}`); button.title = item.emoji; button.addEventListener('pointerdown', (event) => event.preventDefault()); button.addEventListener('click', () => onSelect(item.emoji)); const preview = document.createElement('span'); preview.className = 'emoji-picker-preview'; appendAnimatedAsset(preview, item); button.append(preview); return button; } export function isTelegramAnimatedEmoji(emoji) { return EMOJI_ASSET_BY_CHAR.has(emoji); } export function createTelegramAnimatedEmoji(emoji, { className = '' } = {}) { const item = EMOJI_ASSET_BY_CHAR.get(emoji); const root = document.createElement('span'); root.className = `telegram-emoji ${className}`.trim(); root.setAttribute('aria-label', emoji); root.setAttribute('role', 'img'); if (!item) { root.textContent = emoji; return root; } appendAnimatedAsset(root, item); return root; } export function appendTelegramAnimatedText(root, text, { className = '' } = {}) { const source = String(text || ''); let cursor = 0; let found = false; TELEGRAM_EMOJI_MATCHER.lastIndex = 0; for (const match of source.matchAll(TELEGRAM_EMOJI_MATCHER)) { const index = Number(match.index || 0); if (index > cursor) root.append(document.createTextNode(source.slice(cursor, index))); root.append(createTelegramAnimatedEmoji(match[0], { className })); cursor = index + match[0].length; found = true; } if (!found) { root.textContent = source; return false; } if (cursor < source.length) root.append(document.createTextNode(source.slice(cursor))); return true; } export function createEmojiPicker({ onSelect }) { const root = document.createElement('section'); root.className = 'emoji-picker'; root.setAttribute('aria-label', 'Emoji picker'); const recentSection = document.createElement('section'); recentSection.className = 'emoji-picker-section emoji-picker-recent'; const recentTitle = document.createElement('strong'); recentTitle.className = 'emoji-picker-section-title'; recentTitle.textContent = 'Recent'; const recentGrid = document.createElement('div'); recentGrid.className = 'emoji-picker-grid'; recentSection.append(recentTitle, recentGrid); const categoryTabs = document.createElement('div'); categoryTabs.className = 'emoji-picker-tabs'; categoryTabs.setAttribute('role', 'tablist'); const categorySection = document.createElement('section'); categorySection.className = 'emoji-picker-section'; const categoryTitle = document.createElement('strong'); categoryTitle.className = 'emoji-picker-section-title'; const categoryGrid = document.createElement('div'); categoryGrid.className = 'emoji-picker-grid'; categorySection.append(categoryTitle, categoryGrid); let activeGroup = TELEGRAM_EMOJI_GROUPS[0]; const tabs = new Map(); const rememberAndSelect = (emoji) => { const recent = [emoji, ...loadRecent().filter((item) => item !== emoji)]; saveRecent(recent); renderRecent(); onSelect?.(emoji); }; const renderRecent = () => { const recentItems = (loadRecent().length ? loadRecent() : DEFAULT_RECENT) .map((emoji) => EMOJI_ASSET_BY_CHAR.get(emoji)) .filter(Boolean); stopAllTelegramEmojiAnimations(); recentGrid.replaceChildren(...recentItems.map((item) => createEmojiButton(item, rememberAndSelect))); }; const renderGroup = () => { categoryTitle.textContent = activeGroup.label; stopAllTelegramEmojiAnimations(); categoryGrid.replaceChildren(...activeGroup.items.map((item) => createEmojiButton(item, rememberAndSelect))); tabs.forEach((tab, groupId) => { const active = groupId === activeGroup.id; tab.classList.toggle('is-active', active); tab.setAttribute('aria-selected', String(active)); }); }; TELEGRAM_EMOJI_GROUPS.forEach((group) => { const tab = document.createElement('button'); tab.type = 'button'; tab.className = 'emoji-picker-tab'; tab.textContent = group.icon; tab.title = group.label; tab.setAttribute('aria-label', group.label); tab.setAttribute('role', 'tab'); tab.addEventListener('click', () => { activeGroup = group; renderGroup(); }); tabs.set(group.id, tab); categoryTabs.append(tab); }); root.append(recentSection, categoryTabs, categorySection); renderRecent(); renderGroup(); return root; }