feat: Add emoji and alot of other fixes
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
/* 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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user