From 62ae5307a4337b54283e712467c82a7c64db45fe Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 28 Jun 2026 23:19:14 +0200 Subject: [PATCH] fix(settings): constrain release note images --- ...ate-release-notes-dialog.component.spec.ts | 21 ++++++++++++ ...p-update-release-notes-dialog.component.ts | 33 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/settings/app-update-release-notes-dialog.component.spec.ts b/apps/web/src/app/settings/app-update-release-notes-dialog.component.spec.ts index 7a3de9e6d..acc5d24f1 100644 --- a/apps/web/src/app/settings/app-update-release-notes-dialog.component.spec.ts +++ b/apps/web/src/app/settings/app-update-release-notes-dialog.component.spec.ts @@ -70,6 +70,27 @@ describe('AppUpdateReleaseNotesDialogComponent', () => { ).toContain('desktop updater'); }); + it('constrains rendered markdown images to the dialog width', async () => { + ( + window.electron.getAppUpdateReleaseNotes as jest.Mock + ).mockResolvedValueOnce({ + ...releaseNotes, + bodyMarkdown: + '![Application screenshot](https://example.com/screenshot.png)', + }); + + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const image = fixture.nativeElement.querySelector( + '[data-test-id="release-notes-body"] img' + ) as HTMLImageElement; + + expect(image).toBeTruthy(); + expect(image.classList).toContain('release-notes-dialog__image'); + }); + it('loads previous notes lazily without closing the dialog', async () => { fixture.detectChanges(); await fixture.whenStable(); diff --git a/apps/web/src/app/settings/app-update-release-notes-dialog.component.ts b/apps/web/src/app/settings/app-update-release-notes-dialog.component.ts index e54421ee9..32232f60a 100644 --- a/apps/web/src/app/settings/app-update-release-notes-dialog.component.ts +++ b/apps/web/src/app/settings/app-update-release-notes-dialog.component.ts @@ -5,6 +5,7 @@ import { OnInit, SecurityContext, signal, + ViewEncapsulation, } from '@angular/core'; import { DatePipe } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; @@ -24,11 +25,28 @@ import { ElectronBridgeAppUpdateReleaseNotesRequest, } from '@iptvnator/shared/interfaces'; +const RELEASE_NOTES_IMAGE_CLASS = 'release-notes-dialog__image'; + export interface AppUpdateReleaseNotesDialogData { initialVersion?: string; fallbackToLatest?: boolean; } +function decorateReleaseNotesHtml(html: string): string { + if (typeof document === 'undefined') { + return html; + } + + const template = document.createElement('template'); + template.innerHTML = html; + + template.content.querySelectorAll('img').forEach((image) => { + image.classList.add(RELEASE_NOTES_IMAGE_CLASS); + }); + + return template.innerHTML; +} + @Component({ selector: 'app-update-release-notes-dialog', imports: [ @@ -39,6 +57,7 @@ export interface AppUpdateReleaseNotesDialogData { DatePipe, TranslatePipe, ], + encapsulation: ViewEncapsulation.None, template: `

{{ 'SETTINGS.APP_UPDATE_RELEASE_NOTES' | translate }} @@ -177,6 +196,16 @@ export interface AppUpdateReleaseNotesDialogData { padding: 1px 4px; background: var(--app-hover-overlay); } + + .release-notes-dialog__body img, + .release-notes-dialog__image { + display: block; + box-sizing: border-box; + max-width: 100%; + height: auto; + margin: 16px auto; + object-fit: contain; + } `, ], }) @@ -195,8 +224,10 @@ export class AppUpdateReleaseNotesDialogComponent implements OnInit { readonly renderedMarkdown = computed(() => { const markdown = this.notes()?.bodyMarkdown ?? ''; const html = marked.parse(markdown, { async: false }) as string; + const sanitizedHtml = + this.sanitizer.sanitize(SecurityContext.HTML, html) ?? ''; - return this.sanitizer.sanitize(SecurityContext.HTML, html) ?? ''; + return decorateReleaseNotesHtml(sanitizedHtml); }); ngOnInit(): void {