fix typing indicator on wrong server
Some checks failed
Queue Release Build / build-linux (push) Blocked by required conditions
Queue Release Build / prepare (push) Successful in 15s
Deploy Web Apps / deploy (push) Successful in 16m15s
Queue Release Build / finalize (push) Has been cancelled
Queue Release Build / build-windows (push) Has been cancelled
Some checks failed
Queue Release Build / build-linux (push) Blocked by required conditions
Queue Release Build / prepare (push) Successful in 15s
Deploy Web Apps / deploy (push) Successful in 16m15s
Queue Release Build / finalize (push) Has been cancelled
Queue Release Build / build-windows (push) Has been cancelled
This commit is contained in:
@@ -5,6 +5,10 @@ import {
|
||||
signal,
|
||||
effect
|
||||
} from '@angular/core';
|
||||
import {
|
||||
NavigationEnd,
|
||||
Router
|
||||
} from '@angular/router';
|
||||
import { take } from 'rxjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { WebRTCService } from './webrtc.service';
|
||||
@@ -12,6 +16,7 @@ import { Store } from '@ngrx/store';
|
||||
import { selectCurrentRoomName } from '../../store/rooms/rooms.selectors';
|
||||
import { DatabaseService } from './database.service';
|
||||
import { recordDebugNetworkFileChunk } from './debug-network-metrics.service';
|
||||
import { ROOM_URL_PATTERN } from '../constants';
|
||||
import type {
|
||||
ChatAttachmentAnnouncement,
|
||||
ChatAttachmentMeta,
|
||||
@@ -145,9 +150,14 @@ export class AttachmentService {
|
||||
private readonly webrtc = inject(WebRTCService);
|
||||
private readonly ngrxStore = inject(Store);
|
||||
private readonly database = inject(DatabaseService);
|
||||
private readonly router = inject(Router);
|
||||
|
||||
/** Primary index: `messageId → Attachment[]`. */
|
||||
private attachmentsByMessage = new Map<string, Attachment[]>();
|
||||
/** Runtime cache of `messageId → roomId` for attachment gating. */
|
||||
private messageRoomIds = new Map<string, string>();
|
||||
/** Room currently being watched in the router, or `null` outside room routes. */
|
||||
private watchedRoomId: string | null = this.extractWatchedRoomId(this.router.url);
|
||||
|
||||
/** Incremented on every mutation so signal consumers re-render. */
|
||||
updated = signal<number>(0);
|
||||
@@ -190,6 +200,24 @@ export class AttachmentService {
|
||||
this.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getElectronApi(): AttachmentElectronApi | undefined {
|
||||
@@ -201,6 +229,44 @@ export class AttachmentService {
|
||||
return this.attachmentsByMessage.get(messageId) ?? [];
|
||||
}
|
||||
|
||||
/** Cache the room that owns a message so background downloads can be gated by the watched server. */
|
||||
rememberMessageRoom(messageId: string, roomId: string): void {
|
||||
if (!messageId || !roomId)
|
||||
return;
|
||||
|
||||
this.messageRoomIds.set(messageId, roomId);
|
||||
}
|
||||
|
||||
/** Queue best-effort auto-download checks for a message's eligible attachments. */
|
||||
queueAutoDownloadsForMessage(messageId: string, attachmentId?: string): void {
|
||||
void this.requestAutoDownloadsForMessage(messageId, attachmentId);
|
||||
}
|
||||
|
||||
/** Auto-request eligible missing attachments for the currently watched room. */
|
||||
async requestAutoDownloadsForRoom(roomId: string): Promise<void> {
|
||||
if (!roomId || !this.isRoomWatched(roomId))
|
||||
return;
|
||||
|
||||
if (this.database.isReady()) {
|
||||
const messages = await this.database.getMessages(roomId, 500, 0);
|
||||
|
||||
for (const message of messages) {
|
||||
this.rememberMessageRoom(message.id, message.roomId);
|
||||
await this.requestAutoDownloadsForMessage(message.id);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [messageId] of this.attachmentsByMessage) {
|
||||
const attachmentRoomId = await this.resolveMessageRoomId(messageId);
|
||||
|
||||
if (attachmentRoomId === roomId) {
|
||||
await this.requestAutoDownloadsForMessage(messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove every attachment associated with a message. */
|
||||
async deleteForMessage(messageId: string): Promise<void> {
|
||||
const attachments = this.attachmentsByMessage.get(messageId) ?? [];
|
||||
@@ -219,6 +285,7 @@ export class AttachmentService {
|
||||
}
|
||||
|
||||
this.attachmentsByMessage.delete(messageId);
|
||||
this.messageRoomIds.delete(messageId);
|
||||
this.clearMessageScopedState(messageId);
|
||||
|
||||
if (hadCachedAttachments) {
|
||||
@@ -276,8 +343,15 @@ export class AttachmentService {
|
||||
* @param attachmentMap - Map of `messageId → AttachmentMeta[]` from peer.
|
||||
*/
|
||||
registerSyncedAttachments(
|
||||
attachmentMap: Record<string, AttachmentMeta[]>
|
||||
attachmentMap: Record<string, AttachmentMeta[]>,
|
||||
messageRoomIds?: Record<string, string>
|
||||
): void {
|
||||
if (messageRoomIds) {
|
||||
for (const [messageId, roomId] of Object.entries(messageRoomIds)) {
|
||||
this.rememberMessageRoom(messageId, roomId);
|
||||
}
|
||||
}
|
||||
|
||||
const newAttachments: Attachment[] = [];
|
||||
|
||||
for (const [messageId, metas] of Object.entries(attachmentMap)) {
|
||||
@@ -306,6 +380,7 @@ export class AttachmentService {
|
||||
|
||||
for (const attachment of newAttachments) {
|
||||
void this.persistAttachmentMeta(attachment);
|
||||
this.queueAutoDownloadsForMessage(attachment.messageId, attachment.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,9 +450,9 @@ export class AttachmentService {
|
||||
* message to all connected peers.
|
||||
*
|
||||
* 1. Each file is assigned a UUID.
|
||||
* 2. A `file-announce` event is broadcast to peers.
|
||||
* 3. Inline-preview media ≤ {@link MAX_AUTO_SAVE_SIZE_BYTES}
|
||||
* are immediately streamed as chunked base-64.
|
||||
* 2. A `file-announce` event is broadcast to peers.
|
||||
* 3. Peers watching the message's server can request any
|
||||
* auto-download-eligible media on demand.
|
||||
*
|
||||
* @param messageId - ID of the parent message.
|
||||
* @param files - Array of user-selected `File` objects.
|
||||
@@ -437,10 +512,6 @@ export class AttachmentService {
|
||||
|
||||
this.webrtc.broadcastMessage(fileAnnounceEvent);
|
||||
|
||||
// Auto-stream small inline-preview media
|
||||
if (this.isMedia(attachment) && attachment.size <= MAX_AUTO_SAVE_SIZE_BYTES) {
|
||||
await this.streamFileToPeers(messageId, fileId, file);
|
||||
}
|
||||
}
|
||||
|
||||
const existingList = this.attachmentsByMessage.get(messageId) ?? [];
|
||||
@@ -482,6 +553,7 @@ export class AttachmentService {
|
||||
this.attachmentsByMessage.set(messageId, list);
|
||||
this.touch();
|
||||
void this.persistAttachmentMeta(attachment);
|
||||
this.queueAutoDownloadsForMessage(messageId, attachment.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -772,6 +844,38 @@ export class AttachmentService {
|
||||
return `${messageId}:${fileId}`;
|
||||
}
|
||||
|
||||
private async requestAutoDownloadsForMessage(messageId: string, attachmentId?: string): Promise<void> {
|
||||
if (!messageId)
|
||||
return;
|
||||
|
||||
const roomId = await this.resolveMessageRoomId(messageId);
|
||||
|
||||
if (!roomId || !this.isRoomWatched(roomId) || this.webrtc.getConnectedPeers().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const attachments = this.attachmentsByMessage.get(messageId) ?? [];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
if (attachmentId && attachment.id !== attachmentId)
|
||||
continue;
|
||||
|
||||
if (!this.shouldAutoRequestWhenWatched(attachment))
|
||||
continue;
|
||||
|
||||
if (attachment.available)
|
||||
continue;
|
||||
|
||||
if ((attachment.receivedBytes ?? 0) > 0)
|
||||
continue;
|
||||
|
||||
if (this.pendingRequests.has(this.buildRequestKey(messageId, attachment.id)))
|
||||
continue;
|
||||
|
||||
this.requestFromAnyPeer(messageId, attachment);
|
||||
}
|
||||
}
|
||||
|
||||
private clearMessageScopedState(messageId: string): void {
|
||||
const scopedPrefix = `${messageId}:`;
|
||||
|
||||
@@ -867,6 +971,12 @@ export class AttachmentService {
|
||||
attachment.mime.startsWith('audio/');
|
||||
}
|
||||
|
||||
/** Auto-download only the assets that already supported eager loading when watched. */
|
||||
private shouldAutoRequestWhenWatched(attachment: Attachment): boolean {
|
||||
return attachment.isImage ||
|
||||
(this.isMedia(attachment) && attachment.size <= MAX_AUTO_SAVE_SIZE_BYTES);
|
||||
}
|
||||
|
||||
/** Check whether a completed download should be cached on disk. */
|
||||
private shouldPersistDownloadedAttachment(attachment: Attachment): boolean {
|
||||
return attachment.size <= MAX_AUTO_SAVE_SIZE_BYTES ||
|
||||
@@ -1167,6 +1277,38 @@ export class AttachmentService {
|
||||
} catch { /* load is best-effort */ }
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private async resolveMessageRoomId(messageId: string): Promise<string | null> {
|
||||
const cachedRoomId = this.messageRoomIds.get(messageId);
|
||||
|
||||
if (cachedRoomId)
|
||||
return cachedRoomId;
|
||||
|
||||
if (!this.database.isReady())
|
||||
return null;
|
||||
|
||||
try {
|
||||
const message = await this.database.getMessageById(messageId);
|
||||
|
||||
if (!message?.roomId)
|
||||
return null;
|
||||
|
||||
this.rememberMessageRoom(messageId, message.roomId);
|
||||
return message.roomId;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** One-time migration from localStorage to the database. */
|
||||
private async migrateFromLocalStorage(): Promise<void> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user