Improve attachment memory safety, downloads, and high-memory alert UX.
All checks were successful
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
All checks were successful
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:
@@ -15,7 +15,6 @@ import { Store } from '@ngrx/store';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { map } from 'rxjs';
|
||||
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
|
||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||
import { ViewportService } from '../../../../core/platform';
|
||||
import {
|
||||
BottomSheetComponent,
|
||||
@@ -23,7 +22,11 @@ import {
|
||||
UserAvatarComponent
|
||||
} from '../../../../shared';
|
||||
import { DirectCallService } from '../../../direct-call';
|
||||
import { Attachment, AttachmentFacade } from '../../../attachment';
|
||||
import {
|
||||
Attachment,
|
||||
AttachmentDownloadService,
|
||||
AttachmentFacade
|
||||
} from '../../../attachment';
|
||||
import { ThemeNodeDirective } from '../../../theme';
|
||||
import { DirectMessageService } from '../../application/services/direct-message.service';
|
||||
import { isConversationBound } from './dm-chat.rules';
|
||||
@@ -88,7 +91,7 @@ export class DmChatComponent {
|
||||
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly store = inject(Store);
|
||||
private readonly electronBridge = inject(ElectronBridgeService);
|
||||
private readonly attachmentDownload = inject(AttachmentDownloadService);
|
||||
private readonly attachments = inject(AttachmentFacade);
|
||||
private readonly klipy = inject(KlipyService);
|
||||
private readonly linkMetadata = inject(LinkMetadataService);
|
||||
@@ -485,49 +488,7 @@ export class DmChatComponent {
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -599,48 +560,6 @@ export class DmChatComponent {
|
||||
return `${messageId}:${url}`;
|
||||
}
|
||||
|
||||
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 peerUserFor(conversation: NonNullable<ReturnType<typeof this.conversation>>): User | null {
|
||||
if (conversation.kind === 'group' || conversation.participants.length > 2) {
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user