fix(comments): open image/PDF attachments in-app instead of trapping the app - #829
Conversation
…the app Comment attachments rendered as a bare <a href="memry-file://…">. Clicking drove a main-frame navigation that decideFrameNavigation allowed for the memry-file scheme, replacing the React app with the protocol handler's raw bytes — images trapped the app (only a restart recovered) and PDFs rendered black while the OS PDF app opened (issue #799). - New comment-attachments.tsx: images show inline <img> thumbnails; clicking an image or PDF opens the existing in-app ImageViewer/PdfViewer in a Dialog (Esc / click-outside / X close). Non-viewable files open in the OS default app via window.open -> shell.openPath. No navigable memry-file anchors. PdfViewer is lazy-loaded (react-pdf crashes jsdom at module load). - Defense-in-depth: main-frame memry-file navigations now resolve to a new 'open-file' decision (subframes still allow), routed through a shared openMemryFileInOs helper reused by setWindowOpenHandler. No DB/contract/IPC/vault-format change; fully backward-compatible.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
There was a problem hiding this comment.
Pull request overview
This PR fixes a UX-breaking bug where clicking comment image/PDF attachments triggered a main-frame navigation to memry-file://…, unmounting the React app and “trapping” users in a raw image/blank PDF document. It introduces in-app viewers for comment attachments and hardens main-frame navigation handling as defense-in-depth.
Changes:
- Replace comment attachment rendering with inline image thumbnails and an in-app
Dialog+ImageViewer/ lazyPdfViewerfor image/PDF attachments. - Route non-viewable attachments through
window.open→ ElectronsetWindowOpenHandler→ allowlistedshell.openPath(OS default app). - Harden navigation policy so main-frame
memry-file:navigations become an explicit'open-file'decision and are routed through a shared allowlist-checked helper.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/i18n/src/locales/en/notes.json | Adds ARIA label strings for opening image/attachment actions in comment UI. |
| apps/docs/src/user-guide/notes/editing.md | Documents the new comment attachment behavior (thumbnail + in-app viewers; OS open for other types). |
| apps/desktop/src/renderer/src/components/note/review/review-card.tsx | Removes the old inline anchor-based attachments renderer and wires in the new component. |
| apps/desktop/src/renderer/src/components/note/review/comment-attachments.tsx | New attachment renderer: thumbnails + dialog viewers for image/PDF; OS open for other file types; no navigable memry-file:// anchors. |
| apps/desktop/src/renderer/src/components/note/review/comment-attachments.test.tsx | Adds unit tests for classification and ensuring correct viewer/OS routing behavior. |
| apps/desktop/src/main/lib/frame-navigation.ts | Introduces 'open-file' decision for main-frame memry-file: navigations to prevent SPA replacement/trap. |
| apps/desktop/src/main/lib/frame-navigation.test.ts | Updates tests to assert main-frame memry-file: becomes 'open-file' while subframes remain allowed. |
| apps/desktop/src/main/index.ts | Adds openMemryFileInOs helper (shared allowlist logic) and wires it into both the frame navigation guard and setWindowOpenHandler. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…mpact cards Image and file attachments shared a wrapping flex row, so a PDF pill stretched to the image thumbnail's height. Stack attachments one per line and render non-image files as a compact bordered card (file icon + name + size) instead of a full-height background pill.
| it('hands a non-viewable file to the OS via window.open — no in-app viewer, no trap', () => { | ||
| const openSpy = vi.spyOn(window, 'open').mockReturnValue(null) | ||
| const path = 'memry-file://local/vault/attachments/n1/notes.docx' | ||
| render(<CommentAttachments mark={markWith([att({ name: 'notes.docx', path })])} />) | ||
| fireEvent.click(screen.getByRole('button')) | ||
| expect(openSpy).toHaveBeenCalledWith(path, '_blank', 'noopener,noreferrer') | ||
| expect(screen.queryByTestId('image-viewer')).toBeNull() |
.critic-review-content sets pointer-events: none and only re-enabled it for anchors (the old attachments were <a> links). The new attachment component uses <button>, so clicks fell through to the expand-toggle overlay and the viewer never opened. Re-enable pointer-events for buttons too.
| // App-controlled local scheme. Inline resource loads (<img>/<audio>/<video>, | ||
| // react-pdf fetch) never fire will-frame-navigate, so a memry-file event here | ||
| // is a real navigation. On the main frame that would replace the SPA document | ||
| // with the protocol handler's raw file bytes and trap the app (no in-app back | ||
| // path) — hand the file to the OS instead. Subframes may still load it in-place |
Fixes #799 (part of #795).
Problem
Clicking a comment's image or PDF attachment trapped the app.
CommentAttachmentsrendered each attachment as a bare<a href="memry-file://…">, so clicking drove a main-frame navigation.decideFrameNavigationallowedmemry-file:on the top frame, so Chromium replaced the React app with the protocol handler's raw bytes:Fix
1. Comment attachment rendering (
comment-attachments.tsx, new)<img>thumbnail.ImageViewer/PdfViewerinside aDialog— closes with Esc, click-outside, or the ✕.window.open→setWindowOpenHandler→shell.openPath.memry-file://anchors anywhere.PdfViewerisReact.lazy-loaded —react-pdf/pdf.js crashes at module load outside a real DOM (DOMMatrix is not defined), and this keepsreview-card's static import graph clean for jsdom tests (also defers a heavy dep until a PDF is actually opened).2. Defense-in-depth navigation hardening (
frame-navigation.ts,index.ts)memry-file:navigation now resolves to a new'open-file'decision (subframes still'allow'), routed through a sharedopenMemryFileInOshelper that is also reused bysetWindowOpenHandler. Any future barememry-file://anchor opens in the OS instead of trapping the app.Acceptance
PdfViewer(not a black frame / external app).Notes for reviewers
mimeType/typevia a filename-extension fallback.fileId(they live in a per-noteattachments/<noteId>/folder, not the indexed vault-files table), so the tab-based file-page opener does not apply — the viewers are reused directly in aDialog.Verification
frame-navigation25 ✓ ·comment-attachments11 ✓ · full review dir 46 ✓ · main lib 195 ✓typecheck(web + node) ✓ · IPC/contract/architecture checks ✓ ·eslint0 errors ·i18n:check✓ ·docs:impact --strictcovered ·docs:build✓Manual smoke pending on a packaged/dev run: comment with an image, a PDF, and a
.txt.