220 lines
7.8 KiB
TypeScript
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';
|
|
}
|
|
}
|