/* eslint-disable @typescript-eslint/member-ordering */ import { Component, computed, effect, inject, OnDestroy, output, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideCamera, lucideCheck, lucideChevronDown, lucideGamepad2, lucideMessageCircle, lucidePhone, lucideUserMinus, lucideUserPlus } from '@ng-icons/lucide'; import { UserAvatarComponent } from '../user-avatar/user-avatar.component'; import { ThemeNodeDirective } from '../../../domains/theme'; import { User, UserStatus } from '../../../shared-kernel'; import { selectCurrentUser, selectUsersEntities } from '../../../store/users/users.selectors'; import { UsersActions } from '../../../store/users/users.actions'; import { DirectMessageService } from '../../../domains/direct-message/application/services/direct-message.service'; import { FriendService } from '../../../domains/direct-message/application/services/friend.service'; import { DirectCallService } from '../../../domains/direct-call/application/services/direct-call.service'; import { formatGameActivityElapsed } from '../../../domains/game-activity'; import { ExternalLinkService } from '../../../core/platform/external-link.service'; import { UserStatusService } from '../../../core/services/user-status.service'; import { EditableProfileAvatarSource, PROFILE_AVATAR_ACCEPT_ATTRIBUTE, ProcessedProfileAvatar, ProfileAvatarEditorService, ProfileAvatarFacade } from '../../../domains/profile-avatar'; @Component({ selector: 'app-profile-card-mobile', standalone: true, imports: [ CommonModule, NgIcon, UserAvatarComponent, ThemeNodeDirective ], viewProviders: [ provideIcons({ lucideCamera, lucideCheck, lucideChevronDown, lucideGamepad2, lucideMessageCircle, lucidePhone, lucideUserMinus, lucideUserPlus }) ], templateUrl: './profile-card-mobile.component.html' }) export class ProfileCardMobileComponent implements OnDestroy { readonly user = signal({ id: '', oderId: '', username: '', displayName: '', status: 'offline', role: 'member', joinedAt: 0 }); readonly editable = signal(false); readonly closed = output(); readonly avatarAccept = PROFILE_AVATAR_ACCEPT_ATTRIBUTE; readonly avatarError = signal(null); readonly avatarSaving = signal(false); readonly editingField = signal<'displayName' | 'description' | null>(null); readonly displayNameDraft = signal(''); readonly descriptionDraft = signal(''); readonly showStatusMenu = signal(false); readonly statusOptions: { value: UserStatus | null; label: string; color: string }[] = [ { value: null, label: 'Online', color: 'bg-green-500' }, { value: 'away', label: 'Away', color: 'bg-yellow-500' }, { value: 'busy', label: 'Do Not Disturb', color: 'bg-red-500' }, { value: 'offline', label: 'Invisible', color: 'bg-gray-500' } ]; private readonly store = inject(Store); private readonly router = inject(Router); private readonly directMessages = inject(DirectMessageService); private readonly directCalls = inject(DirectCallService); private readonly friendsService = inject(FriendService); private readonly externalLinks = inject(ExternalLinkService); private readonly userStatus = inject(UserStatusService); private readonly profileAvatar = inject(ProfileAvatarFacade); private readonly profileAvatarEditor = inject(ProfileAvatarEditorService); private readonly users = this.store.selectSignal(selectUsersEntities); private readonly currentUser = this.store.selectSignal(selectCurrentUser); readonly displayedUser = computed(() => { const snapshot = this.user(); const entities = this.users(); const liveUser = entities[snapshot.id] ?? entities[snapshot.oderId]; return liveUser ? { ...snapshot, ...liveUser } : snapshot; }); readonly isSelf = computed(() => { const me = this.currentUser(); const them = this.displayedUser(); if (!me) return false; return me.id === them.id || me.oderId === them.oderId; }); readonly isFriend = computed(() => this.friendsService.friendIds().has(this.displayedUser().id)); readonly activityNow = signal(Date.now()); readonly busy = signal(false); private readonly activityTimer = setInterval(() => this.activityNow.set(Date.now()), 1_000); private readonly syncProfileDrafts = effect( () => { const user = this.displayedUser(); const editingField = this.editingField(); if (editingField !== 'displayName') { this.displayNameDraft.set(user.displayName || ''); } if (editingField !== 'description') { this.descriptionDraft.set(user.description || ''); } }, { allowSignalWrites: true } ); ngOnDestroy(): void { clearInterval(this.activityTimer); } currentStatusColor(): string { switch (this.displayedUser().status) { case 'online': return 'bg-green-500'; case 'away': return 'bg-yellow-500'; case 'busy': return 'bg-red-500'; default: return 'bg-gray-500'; } } currentStatusLabel(): string { switch (this.displayedUser().status) { case 'online': return 'Online'; case 'away': return 'Away'; case 'busy': return 'Do Not Disturb'; case 'offline': return 'Invisible'; case 'disconnected': return 'Offline'; default: return 'Online'; } } gameActivityElapsed(): string { const activity = this.displayedUser().gameActivity; return activity ? formatGameActivityElapsed(activity.startedAt, this.activityNow()) : ''; } openGameStore(event: Event): void { event.stopPropagation(); const url = this.displayedUser().gameActivity?.store?.url; if (url) { this.externalLinks.open(url); } } toggleStatusMenu(): void { this.showStatusMenu.update((open) => !open); } setStatus(status: UserStatus | null): void { this.userStatus.setManualStatus(status); this.showStatusMenu.set(false); } isStatusOptionSelected(status: UserStatus | null): boolean { const currentStatus = this.displayedUser().status; return status === null ? currentStatus === 'online' : currentStatus === status; } onDisplayNameInput(event: Event): void { this.displayNameDraft.set((event.target as HTMLInputElement).value); } onDescriptionInput(event: Event): void { this.descriptionDraft.set((event.target as HTMLTextAreaElement).value); } startEdit(field: 'displayName' | 'description'): void { if (!this.editable() || this.editingField() === field) { return; } this.editingField.set(field); } finishEdit(field: 'displayName' | 'description'): void { if (this.editingField() !== field) { return; } this.commitProfileDrafts(); this.editingField.set(null); } pickAvatar(fileInput: HTMLInputElement): void { if (!this.editable() || this.avatarSaving()) { return; } this.avatarError.set(null); fileInput.click(); } async onAvatarSelected(event: Event): Promise { const input = event.target as HTMLInputElement; const file = input.files?.[0]; let source: EditableProfileAvatarSource | null = null; input.value = ''; if (!file) { return; } const validationError = this.profileAvatar.validateFile(file); if (validationError) { this.avatarError.set(validationError); return; } try { source = await this.profileAvatar.prepareEditableSource(file); const avatar = await this.profileAvatarEditor.open(source); if (!avatar) { return; } await this.applyAvatar(avatar); } catch { this.avatarError.set('Failed to open selected image.'); } finally { this.profileAvatar.releaseEditableSource(source); } } async applyAvatar(avatar: ProcessedProfileAvatar): Promise { const currentUser = this.displayedUser(); this.avatarSaving.set(true); this.avatarError.set(null); try { await this.profileAvatar.persistProcessedAvatar(currentUser, avatar); const updates = this.profileAvatar.buildAvatarUpdates(avatar); this.store.dispatch(UsersActions.updateCurrentUserAvatar({ avatar: updates })); this.user.update((user) => ({ ...user, ...updates })); } catch { this.avatarError.set('Failed to save profile image.'); } finally { this.avatarSaving.set(false); } } async startChat(): Promise { if (this.busy() || this.isSelf()) return; this.busy.set(true); try { const conversation = await this.directMessages.createConversation(this.displayedUser()); await this.router.navigate(['/dm', conversation.id]); this.closed.emit(undefined); } finally { this.busy.set(false); } } async startCall(): Promise { if (this.busy() || this.isSelf()) return; this.busy.set(true); try { await this.directCalls.startCall(this.displayedUser()); this.closed.emit(undefined); } finally { this.busy.set(false); } } async toggleFriend(): Promise { if (this.busy() || this.isSelf()) return; this.busy.set(true); try { await this.friendsService.toggleFriend(this.displayedUser().id); } finally { this.busy.set(false); } } private commitProfileDrafts(): void { if (!this.editable()) { return; } const displayName = this.normalizeDisplayName(this.displayNameDraft()); if (!displayName) { this.displayNameDraft.set(this.user().displayName || ''); return; } const user = this.displayedUser(); const description = this.normalizeDescription(this.descriptionDraft()); if (displayName === this.normalizeDisplayName(user.displayName) && description === this.normalizeDescription(user.description)) { return; } const profile = { displayName, description, profileUpdatedAt: Date.now() }; this.store.dispatch(UsersActions.updateCurrentUserProfile({ profile })); this.user.update((user) => ({ ...user, ...profile })); } private normalizeDisplayName(value: string | undefined): string { return value?.trim().replace(/\s+/g, ' ') || ''; } private normalizeDescription(value: string | undefined): string | undefined { const normalized = value?.trim(); return normalized || undefined; } }