/* eslint-disable @typescript-eslint/member-ordering */ import { CommonModule } from '@angular/common'; import { Component, ElementRef, HostListener, computed, effect, inject, input, output, signal } from '@angular/core'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucidePlus, lucideSearch, lucideSmile, lucideUpload, lucideX } from '@ng-icons/lucide'; import { CustomEmoji, EmojiShortcutEntry } from '../../../../shared-kernel'; import { CustomEmojiService } from '../../application/custom-emoji.service'; import { CUSTOM_EMOJI_ACCEPT_ATTRIBUTE, UNICODE_EMOJI_PICKER_ENTRIES, buildCustomEmojiMessageToken, filterCustomEmojisForPicker, filterUnicodeEmojiPickerEntries, normalizeEmojiPickerSearchQuery } from '../../domain/custom-emoji.rules'; @Component({ selector: 'app-custom-emoji-picker', standalone: true, imports: [CommonModule, NgIcon], viewProviders: [provideIcons({ lucidePlus, lucideSearch, lucideSmile, lucideUpload, lucideX })], templateUrl: './custom-emoji-picker.component.html' }) export class CustomEmojiPickerComponent { private readonly customEmoji = inject(CustomEmojiService); private readonly host = inject>(ElementRef); readonly currentUserId = input(null); readonly compact = input(true); /** Render the picker panel in normal document flow for bottom-sheet embedding. */ readonly inline = input(false); readonly emojiSelected = output(); readonly dismissed = output(); readonly acceptAttribute = CUSTOM_EMOJI_ACCEPT_ATTRIBUTE; readonly modalOpen = signal(false); readonly uploadError = signal(null); readonly uploading = signal(false); readonly shortcuts = this.customEmoji.shortcutEntries; readonly customEmojis = this.customEmoji.emojis; readonly searchQuery = signal(''); readonly filteredUnicodeEntries = computed(() => filterUnicodeEmojiPickerEntries( UNICODE_EMOJI_PICKER_ENTRIES, this.searchQuery() )); readonly filteredCustomEmojis = computed(() => filterCustomEmojisForPicker( this.customEmojis(), this.searchQuery() )); readonly hasActiveSearch = computed(() => normalizeEmojiPickerSearchQuery(this.searchQuery()).length > 0); readonly showEmptySearchState = computed(() => this.hasActiveSearch() && this.filteredUnicodeEntries().length === 0 && this.filteredCustomEmojis().length === 0); private readonly loadForUser = effect(() => { void this.customEmoji.loadForUser(this.currentUserId()); }); setSearchQuery(query: string): void { this.searchQuery.set(query); } onSearchInput(event: Event): void { this.setSearchQuery((event.target as HTMLInputElement).value); } selectShortcut(entry: EmojiShortcutEntry): void { this.customEmoji.recordUsage(entry, this.currentUserId()); this.emojiSelected.emit(entry.kind === 'custom' ? buildCustomEmojiMessageToken(entry.emoji) : entry.emoji); } selectUnicode(emoji: string): void { this.customEmoji.recordUsage({ kind: 'unicode', key: `unicode:${emoji}`, emoji, label: emoji }, this.currentUserId()); this.emojiSelected.emit(emoji); this.modalOpen.set(false); } selectCustom(emoji: CustomEmoji): void { this.customEmoji.recordUsage({ kind: 'custom', key: `custom:${emoji.id}`, emoji, label: emoji.name }, this.currentUserId()); this.emojiSelected.emit(buildCustomEmojiMessageToken(emoji)); this.modalOpen.set(false); } @HostListener('document:click', ['$event']) onDocumentClick(event: MouseEvent): void { const target = event.target; if (target == null || this.host.nativeElement.contains(target as Node)) { return; } this.dismiss(); } @HostListener('document:keydown.escape') onEscape(): void { this.dismiss(); } openModal(): void { this.modalOpen.set(true); } closeModal(): void { this.dismiss(); } dismiss(): void { this.modalOpen.set(false); this.searchQuery.set(''); this.dismissed.emit(); } async uploadEmoji(event: Event): Promise { const inputElement = event.target as HTMLInputElement; const file = inputElement.files?.[0]; const userId = this.currentUserId(); inputElement.value = ''; if (!file || !userId) { return; } this.uploadError.set(null); this.uploading.set(true); try { const emoji = await this.customEmoji.createFromFile(file, userId); this.selectCustom(emoji); } catch (error) { this.uploadError.set(error instanceof Error ? error.message : 'Unable to upload emoji.'); } finally { this.uploading.set(false); } } }