Files
Toju/toju-app/src/app/domains/custom-emoji/feature/custom-emoji-picker/custom-emoji-picker.component.ts

163 lines
4.6 KiB
TypeScript

/* 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<HTMLElement>>(ElementRef);
readonly currentUserId = input<string | null>(null);
readonly compact = input(true);
/** Render the picker panel in normal document flow for bottom-sheet embedding. */
readonly inline = input(false);
readonly emojiSelected = output<string>();
readonly dismissed = output();
readonly acceptAttribute = CUSTOM_EMOJI_ACCEPT_ATTRIBUTE;
readonly modalOpen = signal(false);
readonly uploadError = signal<string | null>(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<void> {
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);
}
}
}