Improve attachment memory safety, downloads, and high-memory alert UX.
Queue Release Build / prepare (push) Successful in 20s
Deploy Web Apps / deploy (push) Successful in 9m2s
Queue Release Build / build-windows (push) Successful in 28m8s
Queue Release Build / build-linux (push) Successful in 47m26s
Queue Release Build / build-android (push) Successful in 19m52s
Queue Release Build / finalize (push) Successful in 4m42s
Queue Release Build / prepare (push) Successful in 20s
Deploy Web Apps / deploy (push) Successful in 9m2s
Queue Release Build / build-windows (push) Successful in 28m8s
Queue Release Build / build-linux (push) Successful in 47m26s
Queue Release Build / build-android (push) Successful in 19m52s
Queue Release Build / finalize (push) Successful in 4m42s
Stream large receives to disk with chunk acks to cap renderer RAM, evict off-screen display blobs, and route exports through a disk-aware download service. Fix the high-memory dialog (backdrop dismiss, copy, log actions), allow diagnostics paths in the path jail, and restore persisted image hydration after reload. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,11 +12,14 @@ import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||
import { ViewportService } from '../../../../core/platform';
|
||||
import { BottomSheetComponent } from '../../../../shared';
|
||||
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
||||
import { Attachment, AttachmentFacade } from '../../../attachment';
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentDownloadService,
|
||||
AttachmentFacade
|
||||
} from '../../../attachment';
|
||||
import { KlipyGif, KlipyService } from '../../application/services/klipy.service';
|
||||
import { MessagesActions } from '../../../../store/messages/messages.actions';
|
||||
import {
|
||||
@@ -69,10 +72,10 @@ export class ChatMessagesComponent {
|
||||
@ViewChild(ChatMessageComposerComponent) composer?: ChatMessageComposerComponent;
|
||||
@ViewChild(ChatMessageListComponent) messageList?: ChatMessageListComponent;
|
||||
|
||||
private readonly electronBridge = inject(ElectronBridgeService);
|
||||
private readonly store = inject(Store);
|
||||
private readonly webrtc = inject(RealtimeSessionFacade);
|
||||
private readonly attachmentsSvc = inject(AttachmentFacade);
|
||||
private readonly attachmentDownload = inject(AttachmentDownloadService);
|
||||
private readonly klipy = inject(KlipyService);
|
||||
private readonly viewport = inject(ViewportService);
|
||||
|
||||
@@ -300,6 +303,7 @@ export class ChatMessagesComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.attachmentsSvc.pinDisplayBlobs(attachments);
|
||||
this.lightboxState.set({
|
||||
attachments,
|
||||
index
|
||||
@@ -307,6 +311,12 @@ export class ChatMessagesComponent {
|
||||
}
|
||||
|
||||
closeLightbox(): void {
|
||||
const state = this.lightboxState();
|
||||
|
||||
if (state) {
|
||||
this.attachmentsSvc.unpinDisplayBlobs(state.attachments);
|
||||
}
|
||||
|
||||
this.lightboxState.set(null);
|
||||
}
|
||||
|
||||
@@ -336,10 +346,17 @@ export class ChatMessagesComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.attachmentsSvc.pinDisplayBlobs(availableImages);
|
||||
this.galleryAttachments.set(availableImages);
|
||||
}
|
||||
|
||||
closeImageGallery(): void {
|
||||
const gallery = this.galleryAttachments();
|
||||
|
||||
if (gallery) {
|
||||
this.attachmentsSvc.unpinDisplayBlobs(gallery);
|
||||
}
|
||||
|
||||
this.galleryAttachments.set(null);
|
||||
}
|
||||
|
||||
@@ -352,46 +369,7 @@ export class ChatMessagesComponent {
|
||||
}
|
||||
|
||||
async downloadAttachment(attachment: Attachment): Promise<void> {
|
||||
if (!attachment.available || !attachment.objectUrl)
|
||||
return;
|
||||
|
||||
const electronApi = this.electronBridge.getApi();
|
||||
|
||||
if (electronApi) {
|
||||
const diskPath = this.getAttachmentDiskPath(attachment);
|
||||
|
||||
if (diskPath && electronApi.saveExistingFileAs) {
|
||||
try {
|
||||
const result = await electronApi.saveExistingFileAs(diskPath, attachment.filename);
|
||||
|
||||
if (result.saved || result.cancelled)
|
||||
return;
|
||||
} catch {
|
||||
/* fall back to blob/browser download */
|
||||
}
|
||||
}
|
||||
|
||||
const blob = await this.getAttachmentBlob(attachment);
|
||||
|
||||
if (blob) {
|
||||
try {
|
||||
const result = await electronApi.saveFileAs(attachment.filename, await this.blobToBase64(blob));
|
||||
|
||||
if (result.saved || result.cancelled)
|
||||
return;
|
||||
} catch {
|
||||
/* fall back to browser download */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const link = document.createElement('a');
|
||||
|
||||
link.href = attachment.objectUrl;
|
||||
link.download = attachment.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
await this.attachmentDownload.downloadToUserLocation(attachment);
|
||||
}
|
||||
|
||||
async copyImageToClipboard(attachment: Attachment): Promise<void> {
|
||||
@@ -415,46 +393,6 @@ export class ChatMessagesComponent {
|
||||
return message.senderId === this.currentUser()?.id;
|
||||
}
|
||||
|
||||
private async getAttachmentBlob(attachment: Attachment): Promise<Blob | null> {
|
||||
if (!attachment.objectUrl)
|
||||
return null;
|
||||
|
||||
if (attachment.objectUrl.startsWith('file:'))
|
||||
return null;
|
||||
|
||||
try {
|
||||
const response = await fetch(attachment.objectUrl);
|
||||
|
||||
return await response.blob();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private getAttachmentDiskPath(attachment: Attachment): string | null {
|
||||
return attachment.savedPath || attachment.filePath || null;
|
||||
}
|
||||
|
||||
private blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = () => {
|
||||
if (typeof reader.result !== 'string') {
|
||||
reject(new Error('Failed to encode attachment'));
|
||||
return;
|
||||
}
|
||||
|
||||
const [, base64 = ''] = reader.result.split(',', 2);
|
||||
|
||||
resolve(base64);
|
||||
};
|
||||
|
||||
reader.onerror = () => reject(reader.error ?? new Error('Failed to read attachment'));
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
private convertToPng(blob: Blob): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (blob.type === 'image/png') {
|
||||
|
||||
Reference in New Issue
Block a user