|
|
<script lang="ts"> |
|
|
import emojiData from 'unicode-emoji-json/data-ordered-emoji.json'; |
|
|
import emojiNames from 'unicode-emoji-json/data-by-emoji.json'; |
|
|
import EmojiDrawer from './EmojiDrawer.svelte'; |
|
|
import { loadAllEmojiPacks, getAllCustomEmojis } from '../../services/nostr/nip30-emoji.js'; |
|
|
|
|
|
interface Props { |
|
|
open: boolean; |
|
|
onSelect: (emoji: string) => void; |
|
|
onClose: () => void; |
|
|
} |
|
|
|
|
|
let { open, onSelect, onClose }: Props = $props(); |
|
|
|
|
|
let emojis = $state<string[]>([]); |
|
|
let customEmojis = $state<Array<{ shortcode: string; url: string }>>([]); |
|
|
let searchQuery = $state(''); |
|
|
let loadingCustomEmojis = $state(false); |
|
|
|
|
|
// Common emojis to show first |
|
|
const commonEmojis = ['❤️', '🫂','😀', '😂', '😍', '🥰', '😎', '🤔', '👍', '🔥', '✨', '🎉', '💯', '👏', '🙏', '😊', '😢', '😮', '😴', '🤗', '😋']; |
|
|
|
|
|
// Debounce search |
|
|
let searchTimeout: ReturnType<typeof setTimeout> | null = null; |
|
|
|
|
|
function loadEmojis() { |
|
|
const allEmojis: string[] = []; |
|
|
|
|
|
// Add common emojis first |
|
|
allEmojis.push(...commonEmojis); |
|
|
|
|
|
// Add all other emojis |
|
|
for (let i = 0; i < emojiData.length; i++) { |
|
|
const emoji = emojiData[i]; |
|
|
if (typeof emoji === 'string' && emoji.trim() && !commonEmojis.includes(emoji)) { |
|
|
allEmojis.push(emoji); |
|
|
} |
|
|
} |
|
|
|
|
|
emojis = allEmojis; |
|
|
} |
|
|
|
|
|
async function loadCustomEmojis() { |
|
|
loadingCustomEmojis = true; |
|
|
try { |
|
|
await loadAllEmojiPacks(); |
|
|
const allEmojis = getAllCustomEmojis(); |
|
|
console.log(`[EmojiPicker] Loaded ${allEmojis.length} custom emojis`); |
|
|
customEmojis = allEmojis; |
|
|
} catch (error) { |
|
|
console.error('Error loading custom emojis:', error); |
|
|
customEmojis = []; |
|
|
} finally { |
|
|
loadingCustomEmojis = false; |
|
|
} |
|
|
} |
|
|
|
|
|
function handleSearchChange(query: string) { |
|
|
searchQuery = query; |
|
|
|
|
|
// Filter emojis |
|
|
if (searchTimeout) { |
|
|
clearTimeout(searchTimeout); |
|
|
} |
|
|
searchTimeout = setTimeout(async () => { |
|
|
if (!searchQuery.trim()) { |
|
|
loadEmojis(); |
|
|
loadCustomEmojis(); |
|
|
return; |
|
|
} |
|
|
|
|
|
const queryLower = searchQuery.toLowerCase().trim(); |
|
|
|
|
|
// Filter Unicode emojis |
|
|
const allEmojis: string[] = []; |
|
|
allEmojis.push(...commonEmojis); |
|
|
for (let i = 0; i < emojiData.length; i++) { |
|
|
const emoji = emojiData[i]; |
|
|
if (typeof emoji === 'string' && emoji.trim() && !commonEmojis.includes(emoji)) { |
|
|
allEmojis.push(emoji); |
|
|
} |
|
|
} |
|
|
|
|
|
// Filter by emoji name |
|
|
const filtered = allEmojis.filter(emoji => { |
|
|
// Search by emoji name using emojiNames lookup |
|
|
const emojiInfo = (emojiNames as Record<string, { name?: string; slug?: string }>)[emoji]; |
|
|
if (emojiInfo) { |
|
|
const name = emojiInfo.name?.toLowerCase() || ''; |
|
|
const slug = emojiInfo.slug?.toLowerCase() || ''; |
|
|
if (name.includes(queryLower) || slug.includes(queryLower)) { |
|
|
return true; |
|
|
} |
|
|
} |
|
|
|
|
|
// Fallback: search the emoji character itself (though this rarely matches) |
|
|
return emoji.toLowerCase().includes(queryLower); |
|
|
}); |
|
|
|
|
|
emojis = filtered; |
|
|
|
|
|
// Filter custom emojis by shortcode |
|
|
// Ensure custom emojis are loaded, then filter |
|
|
if (customEmojis.length === 0) { |
|
|
await loadCustomEmojis(); |
|
|
} |
|
|
|
|
|
// Filter by shortcode |
|
|
const allCustomEmojis = getAllCustomEmojis(); |
|
|
customEmojis = allCustomEmojis.filter(emoji => |
|
|
emoji.shortcode.toLowerCase().includes(queryLower) |
|
|
); |
|
|
}, 200); |
|
|
} |
|
|
|
|
|
function handleCustomEmojiSelect(shortcode: string) { |
|
|
onSelect(`:${shortcode}:`); |
|
|
onClose(); |
|
|
} |
|
|
|
|
|
// Load emojis when panel opens |
|
|
$effect(() => { |
|
|
if (open) { |
|
|
loadEmojis(); |
|
|
loadCustomEmojis(); |
|
|
searchQuery = ''; |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
|
|
|
<EmojiDrawer |
|
|
{open} |
|
|
title="Choose Emoji" |
|
|
items={emojis} |
|
|
onSelect={onSelect} |
|
|
{onClose} |
|
|
onSearchChange={handleSearchChange} |
|
|
> |
|
|
{#snippet children()} |
|
|
<div class="emoji-picker-content"> |
|
|
{#if emojis.length === 0 && (!searchQuery.trim() || customEmojis.length === 0)} |
|
|
<div class="emoji-empty">No emojis found</div> |
|
|
{:else} |
|
|
{#if emojis.length > 0} |
|
|
<div class="emoji-grid"> |
|
|
{#each emojis as emoji} |
|
|
<button |
|
|
onclick={() => onSelect(emoji)} |
|
|
class="emoji-item" |
|
|
title="Click to insert emoji" |
|
|
> |
|
|
{emoji} |
|
|
</button> |
|
|
{/each} |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
{#if customEmojis.length > 0} |
|
|
<div class="custom-emojis-section"> |
|
|
<div class="custom-emojis-label">Custom</div> |
|
|
<div class="emoji-grid custom-emoji-grid"> |
|
|
{#each customEmojis as emoji} |
|
|
<button |
|
|
onclick={() => handleCustomEmojiSelect(emoji.shortcode)} |
|
|
class="emoji-item custom-emoji-item" |
|
|
title=":{emoji.shortcode}:" |
|
|
> |
|
|
<img src={emoji.url} alt={emoji.shortcode} class="custom-emoji-img" /> |
|
|
</button> |
|
|
{/each} |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
|
|
|
{#if loadingCustomEmojis && !searchQuery.trim()} |
|
|
<div class="emoji-empty">Loading custom emojis...</div> |
|
|
{/if} |
|
|
{/if} |
|
|
</div> |
|
|
{/snippet} |
|
|
</EmojiDrawer> |
|
|
|
|
|
<style> |
|
|
.emoji-picker-content { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 0.75rem; |
|
|
} |
|
|
|
|
|
.custom-emojis-section { |
|
|
margin-top: 0.75rem; |
|
|
padding-top: 0.75rem; |
|
|
border-top: 1px solid var(--fog-border, #e5e7eb); |
|
|
} |
|
|
|
|
|
:global(.dark) .custom-emojis-section { |
|
|
border-top-color: var(--fog-dark-border, #475569); |
|
|
} |
|
|
|
|
|
.custom-emojis-label { |
|
|
font-size: 0.75rem; |
|
|
font-weight: 600; |
|
|
color: var(--fog-text-light, #6b7280); |
|
|
margin-bottom: 0.5rem; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.05em; |
|
|
} |
|
|
|
|
|
:global(.dark) .custom-emojis-label { |
|
|
color: var(--fog-dark-text-light, #9ca3af); |
|
|
} |
|
|
|
|
|
.custom-emoji-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(3rem, 1fr)); |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.custom-emoji-item { |
|
|
padding: 0.5rem; |
|
|
min-height: 3rem; |
|
|
} |
|
|
|
|
|
.custom-emoji-img { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: contain; |
|
|
filter: none !important; |
|
|
} |
|
|
|
|
|
.emoji-empty { |
|
|
text-align: center; |
|
|
padding: 2rem; |
|
|
color: var(--fog-text-light, #9ca3af); |
|
|
font-size: 0.875rem; |
|
|
} |
|
|
|
|
|
:global(.dark) .emoji-empty { |
|
|
color: var(--fog-dark-text-light, #6b7280); |
|
|
} |
|
|
|
|
|
.emoji-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fill, minmax(2.5rem, 1fr)); |
|
|
gap: 0.25rem; |
|
|
} |
|
|
|
|
|
.emoji-item { |
|
|
padding: 0.5rem; |
|
|
border: 1px solid transparent; |
|
|
border-radius: 0.25rem; |
|
|
background: transparent; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
font-size: 1.5rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
min-height: 2.5rem; |
|
|
filter: none !important; |
|
|
} |
|
|
|
|
|
.emoji-item:hover { |
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
border-color: var(--fog-border, #e5e7eb); |
|
|
} |
|
|
|
|
|
:global(.dark) .emoji-item:hover { |
|
|
background: var(--fog-dark-highlight, #374151); |
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
} |
|
|
</style>
|
|
|
|