Files
Toju/toju-app/src/app/domains/direct-message/feature/dm-workspace/dm-conversation-item.component.ts

220 lines
7.8 KiB
TypeScript

/* eslint-disable @typescript-eslint/member-ordering */
import {
Component,
computed,
effect,
inject,
input,
output
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { Store } from '@ngrx/store';
import { NgIcon, provideIcons } from '@ng-icons/core';
import {
lucidePhone,
lucidePhoneCall,
lucideTrash2
} from '@ng-icons/lucide';
import { map } from 'rxjs';
import { UserAvatarComponent } from '../../../../shared';
import { ThemeNodeDirective } from '../../../theme';
import { AttachmentFacade } from '../../../attachment';
import { DirectCallService } from '../../../direct-call';
import { selectAllUsers } from '../../../../store/users/users.selectors';
import type { DirectMessageConversation } from '../../domain/models/direct-message.model';
import type { Attachment } from '../../../attachment';
import type { User } from '../../../../shared-kernel';
import { DirectMessageService } from '../../application/services/direct-message.service';
@Component({
selector: 'app-dm-conversation-item',
standalone: true,
imports: [
CommonModule,
NgIcon,
UserAvatarComponent,
ThemeNodeDirective
],
viewProviders: [provideIcons({ lucidePhone, lucidePhoneCall, lucideTrash2 })],
host: { class: 'block' },
templateUrl: './dm-conversation-item.component.html'
})
export class DmConversationItemComponent {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly store = inject(Store);
private readonly attachments = inject(AttachmentFacade);
private readonly directMessages = inject(DirectMessageService);
private readonly directCalls = inject(DirectCallService);
readonly conversation = input.required<DirectMessageConversation>();
readonly conversationOpened = output<string>();
readonly users = this.store.selectSignal(selectAllUsers);
readonly routeConversationId = toSignal(this.route.paramMap.pipe(map((params) => params.get('conversationId'))), {
initialValue: this.route.snapshot.paramMap.get('conversationId')
});
readonly isSelected = computed(() => this.routeConversationId() === this.conversation().id);
readonly peerName = computed(() => this.resolvePeerName(this.conversation()));
readonly peerAvatarUrl = computed(() => this.resolvePeerAvatarUrl(this.conversation()));
readonly lastMessagePreview = computed(() => this.resolveLastMessagePreview(this.conversation()));
readonly canCall = computed(() => this.canCallConversation(this.conversation()));
readonly callIcon = computed(() => this.conversationCallIcon(this.conversation()));
constructor() {
effect(() => {
const conversation = this.conversation();
const peer = this.peerUser(conversation, this.users());
if (!peer?.avatarUrl) {
this.directMessages.requestPeerAvatarSync(conversation.id);
}
});
}
openConversation(): void {
this.conversationOpened.emit(this.conversation().id);
void this.router.navigate(['/dm', this.conversation().id]);
}
async forgetConversation(event: Event): Promise<void> {
event.stopPropagation();
const conversation = this.conversation();
const conversations = this.directMessages.conversations();
const nextConversation = conversations.find((entry) => entry.id !== conversation.id) ?? null;
await this.directMessages.forgetConversation(conversation.id);
if (this.routeConversationId() === conversation.id) {
await this.router.navigate(nextConversation ? ['/dm', nextConversation.id] : ['/dm']);
}
}
async callConversationPeer(event: Event): Promise<void> {
event.stopPropagation();
await this.directCalls.startConversationCall(this.conversation());
}
formatUnreadCount(count: number): string {
return count > 99 ? '99+' : String(count);
}
private resolvePeerName(conversation: DirectMessageConversation): string {
if (this.isGroupConversation(conversation)) {
return conversation.title || this.groupConversationTitle(conversation);
}
const peerId = this.peerId(conversation);
const knownUser = this.peerUser(conversation);
return peerId ? knownUser?.displayName || conversation.participantProfiles[peerId]?.displayName || peerId : 'Direct Message';
}
private resolvePeerAvatarUrl(conversation: DirectMessageConversation): string | undefined {
if (this.isGroupConversation(conversation)) {
return undefined;
}
const peerId = this.peerId(conversation);
const knownUser = this.peerUser(conversation);
return peerId ? knownUser?.avatarUrl || conversation.participantProfiles[peerId]?.avatarUrl : undefined;
}
private resolveLastMessagePreview(conversation: DirectMessageConversation): string {
const lastMessage = conversation.messages.at(-1);
if (!lastMessage) {
return 'No messages yet';
}
if (lastMessage.isDeleted) {
return 'Message deleted';
}
if (this.isKlipyGif(lastMessage.content)) {
return 'Sent a GIF';
}
this.attachments.updated();
const attachments = this.attachments.getForMessage(lastMessage.id);
if (attachments.length > 0) {
return this.attachmentPreview(attachments);
}
return lastMessage.content || 'Attachment';
}
private conversationCallIcon(conversation: DirectMessageConversation): string {
if (this.isGroupConversation(conversation)) {
return this.directCalls.isCallingConversation(conversation.id) ? 'lucidePhoneCall' : 'lucidePhone';
}
const peer = this.peerUser(conversation);
return peer && this.directCalls.isCallingUser(peer) ? 'lucidePhoneCall' : 'lucidePhone';
}
private canCallConversation(conversation: DirectMessageConversation): boolean {
if (this.isGroupConversation(conversation)) {
return conversation.participants.some((participantId) => participantId !== this.directMessages.currentUserId());
}
return !!this.peerUser(conversation);
}
private peerId(conversation: DirectMessageConversation): string | undefined {
const currentUserId = this.directMessages.currentUserId();
return conversation.participants.find((participantId) => participantId !== currentUserId);
}
private peerUser(conversation: DirectMessageConversation, users = this.users()): User | undefined {
if (this.isGroupConversation(conversation)) {
return undefined;
}
const peerId = this.peerId(conversation);
return peerId ? users.find((user) => user.id === peerId || user.oderId === peerId) : undefined;
}
private isGroupConversation(conversation: DirectMessageConversation): boolean {
return conversation.kind === 'group' || conversation.participants.length > 2;
}
private groupConversationTitle(conversation: DirectMessageConversation): string {
const currentUserId = this.directMessages.currentUserId();
const names = conversation.participants
.filter((participantId) => participantId !== currentUserId)
.map((participantId) => conversation.participantProfiles[participantId]?.displayName || participantId);
if (names.length <= 3) {
return names.join(', ');
}
return `${names.slice(0, 3).join(', ')} +${names.length - 3}`;
}
private isKlipyGif(content: string): boolean {
return /!\[KLIPY GIF\]\([^)]*static\.klipy\.com[^)]*\)/i.test(content.trim());
}
private attachmentPreview(attachments: Attachment[]): string {
if (attachments.some((attachment) => attachment.mime.startsWith('image/'))) {
return 'Sent an image';
}
if (attachments.some((attachment) => attachment.mime.startsWith('video/'))) {
return 'Sent a video';
}
if (attachments.some((attachment) => attachment.mime.startsWith('audio/'))) {
return 'Sent audio';
}
return attachments.length === 1 ? 'Sent an attachment' : 'Sent attachments';
}
}