import { Injectable, effect, inject } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; import { RealtimeSessionFacade } from '../../../../core/realtime'; import { DatabaseService } from '../../../../infrastructure/persistence'; import { ROOM_URL_PATTERN } from '../../../../core/constants'; import { shouldAutoRequestWhenWatched } from '../../domain/logic/attachment.logic'; import type { Attachment, AttachmentMeta } from '../../domain/models/attachment.model'; import type { FileAnnouncePayload, FileCancelPayload, FileChunkPayload, FileNotFoundPayload, FileRequestPayload } from '../../domain/models/attachment-transfer.model'; import { AttachmentPersistenceService } from './attachment-persistence.service'; import { AttachmentRuntimeStore } from './attachment-runtime.store'; import { AttachmentTransferService } from './attachment-transfer.service'; @Injectable({ providedIn: 'root' }) export class AttachmentManagerService { get updated() { return this.runtimeStore.updated; } private readonly webrtc = inject(RealtimeSessionFacade); private readonly router = inject(Router); private readonly database = inject(DatabaseService); private readonly runtimeStore = inject(AttachmentRuntimeStore); private readonly persistence = inject(AttachmentPersistenceService); private readonly transfer = inject(AttachmentTransferService); private watchedRoomId: string | null = this.extractWatchedRoomId(this.router.url); private isDatabaseInitialised = false; private autoDownloadRequestsByRoom = new Map>(); constructor() { effect(() => { if (this.database.isReady() && !this.isDatabaseInitialised) { this.isDatabaseInitialised = true; void this.persistence.initFromDatabase(); } }); this.router.events.subscribe((event) => { if (!(event instanceof NavigationEnd)) { return; } this.watchedRoomId = this.extractWatchedRoomId(event.urlAfterRedirects || event.url); if (this.watchedRoomId) { void this.requestAutoDownloadsForRoom(this.watchedRoomId); } }); this.webrtc.onPeerConnected.subscribe(() => { if (this.watchedRoomId) { void this.requestAutoDownloadsForRoom(this.watchedRoomId); } }); } getForMessage(messageId: string): Attachment[] { return this.runtimeStore.getAttachmentsForMessage(messageId); } rememberMessageRoom(messageId: string, roomId: string): void { if (!messageId || !roomId) return; this.runtimeStore.rememberMessageRoom(messageId, roomId); } queueAutoDownloadsForMessage(messageId: string, attachmentId?: string): void { void this.requestAutoDownloadsForMessage(messageId, attachmentId); } async requestAutoDownloadsForRoom(roomId: string): Promise { if (!roomId || !this.isRoomWatched(roomId) || this.webrtc.getConnectedPeers().length === 0) return; const activeRequest = this.autoDownloadRequestsByRoom.get(roomId); if (activeRequest) { return activeRequest; } const request = this.runAutoDownloadsForRoom(roomId).finally(() => { if (this.autoDownloadRequestsByRoom.get(roomId) === request) { this.autoDownloadRequestsByRoom.delete(roomId); } }); this.autoDownloadRequestsByRoom.set(roomId, request); return request; } async deleteForMessage(messageId: string): Promise { await this.persistence.deleteForMessage(messageId); } getAttachmentMetasForMessages(messageIds: string[]): Record { return this.transfer.getAttachmentMetasForMessages(messageIds); } registerSyncedAttachments( attachmentMap: Record, messageRoomIds?: Record ): void { this.transfer.registerSyncedAttachments(attachmentMap, messageRoomIds); for (const [messageId, attachments] of Object.entries(attachmentMap)) { for (const attachment of attachments) { this.queueAutoDownloadsForMessage(messageId, attachment.id); } } } requestFromAnyPeer(messageId: string, attachment: Attachment): void { this.transfer.requestFromAnyPeer(messageId, attachment); } handleFileNotFound(payload: FileNotFoundPayload): void { this.transfer.handleFileNotFound(payload); } requestImageFromAnyPeer(messageId: string, attachment: Attachment): void { this.transfer.requestImageFromAnyPeer(messageId, attachment); } requestFile(messageId: string, attachment: Attachment): void { this.transfer.requestFile(messageId, attachment); } async publishAttachments( messageId: string, files: File[], uploaderPeerId?: string ): Promise { await this.transfer.publishAttachments(messageId, files, uploaderPeerId); } handleFileAnnounce(payload: FileAnnouncePayload): void { this.transfer.handleFileAnnounce(payload); if (payload.messageId && payload.file?.id) { this.queueAutoDownloadsForMessage(payload.messageId, payload.file.id); } } handleFileChunk(payload: FileChunkPayload): void { this.transfer.handleFileChunk(payload); } async handleFileRequest(payload: FileRequestPayload): Promise { await this.transfer.handleFileRequest(payload); } cancelRequest(messageId: string, attachment: Attachment): void { this.transfer.cancelRequest(messageId, attachment); } handleFileCancel(payload: FileCancelPayload): void { this.transfer.handleFileCancel(payload); } async fulfillRequestWithFile( messageId: string, fileId: string, targetPeerId: string, file: File ): Promise { await this.transfer.fulfillRequestWithFile(messageId, fileId, targetPeerId, file); } private async runAutoDownloadsForRoom(roomId: string): Promise { if (!this.isRoomWatched(roomId)) { return; } if (this.database.isReady()) { const messages = await this.database.getMessages(roomId, 500, 0); for (const message of messages) { this.runtimeStore.rememberMessageRoom(message.id, message.roomId); await this.requestAutoDownloadsForMessage(message.id); } return; } for (const [messageId] of this.runtimeStore.getAttachmentEntries()) { const attachmentRoomId = await this.persistence.resolveMessageRoomId(messageId); if (attachmentRoomId === roomId) { await this.requestAutoDownloadsForMessage(messageId); } } } private async requestAutoDownloadsForMessage(messageId: string, attachmentId?: string): Promise { if (!messageId) return; const roomId = await this.persistence.resolveMessageRoomId(messageId); if (!roomId || !this.isRoomWatched(roomId) || this.webrtc.getConnectedPeers().length === 0) { return; } const attachments = this.runtimeStore.getAttachmentsForMessage(messageId); for (const attachment of attachments) { if (attachmentId && attachment.id !== attachmentId) continue; if (!shouldAutoRequestWhenWatched(attachment)) continue; if (attachment.available) continue; if ((attachment.receivedBytes ?? 0) > 0) continue; if (this.transfer.hasPendingRequest(messageId, attachment.id)) continue; this.transfer.requestFromAnyPeer(messageId, attachment); } } private extractWatchedRoomId(url: string): string | null { const roomMatch = url.match(ROOM_URL_PATTERN); return roomMatch ? roomMatch[1] : null; } private isRoomWatched(roomId: string | null | undefined): boolean { return !!roomId && roomId === this.watchedRoomId; } }