You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

272 lines
7.2 KiB

<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>