247 lines
7.5 KiB
TypeScript
247 lines
7.5 KiB
TypeScript
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<string, Promise<void>>();
|
|
|
|
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<void> {
|
|
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<void> {
|
|
await this.persistence.deleteForMessage(messageId);
|
|
}
|
|
|
|
getAttachmentMetasForMessages(messageIds: string[]): Record<string, AttachmentMeta[]> {
|
|
return this.transfer.getAttachmentMetasForMessages(messageIds);
|
|
}
|
|
|
|
registerSyncedAttachments(
|
|
attachmentMap: Record<string, AttachmentMeta[]>,
|
|
messageRoomIds?: Record<string, string>
|
|
): 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
await this.transfer.fulfillRequestWithFile(messageId, fileId, targetPeerId, file);
|
|
}
|
|
|
|
private async runAutoDownloadsForRoom(roomId: string): Promise<void> {
|
|
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<void> {
|
|
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;
|
|
}
|
|
}
|