Skip to content

✨(frontend) attachments preview#676

Open
jbpenrath wants to merge 1 commit into
mainfrom
feat/attachment-preview
Open

✨(frontend) attachments preview#676
jbpenrath wants to merge 1 commit into
mainfrom
feat/attachment-preview

Conversation

@jbpenrath
Copy link
Copy Markdown
Contributor

@jbpenrath jbpenrath commented May 26, 2026

Purpose

Use the Preview component to allow user to preview message attachment from a thread.

Summary by CodeRabbit

  • New Features

    • Inline attachment preview for supported file types with server-side content validation and clearer refusal reasons
    • Integrated Drive attachment preview and “Open in Drive” actions
    • Fullscreen attachment preview modal with provenance sidebar and preview navigation
  • Updates

    • UI kit dependency bumped
    • Translations extended for preview UI and MIME labels
    • Drive config now exposes a preview URL
  • Bug Fixes

    • Better handling of malformed attachment IDs (400) and concealed-not-found/permission responses
    • Improved long-text wrapping in several UI areas

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6409d66a-8e49-4358-97d1-f4e41044e6d9

📥 Commits

Reviewing files that changed from the base of the PR and between b2b3d50 and 6834740.

⛔ Files ignored due to path filters (3)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
  • src/frontend/src/features/api/gen/blob/blob.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/config_retrieve200_driv_e.ts is excluded by !**/gen/**
📒 Files selected for processing (36)
  • src/backend/core/api/openapi.json
  • src/backend/core/api/viewsets/blob.py
  • src/backend/core/api/viewsets/config.py
  • src/backend/core/enums.py
  • src/backend/core/tests/api/test_attachments.py
  • src/backend/core/tests/api/test_blob_preview.py
  • src/backend/core/tests/api/test_config.py
  • src/backend/core/tests/api/test_drive.py
  • src/backend/messages/settings.py
  • src/frontend/eslint.config.mjs
  • src/frontend/i18next.config.ts
  • src/frontend/package.json
  • src/frontend/public/locales/common/en-US.json
  • src/frontend/public/locales/common/fr-FR.json
  • src/frontend/public/pdf.worker.min.mjs
  • src/frontend/src/features/forms/components/message-form/_index.scss
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx
  • src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx
  • src/frontend/src/features/layouts/components/main/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar-drive-action.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/use-drive-upload.ts
  • src/frontend/src/features/layouts/components/thread-view/components/thread-event/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss
  • src/frontend/src/features/providers/attachment-preview.tsx
  • src/frontend/src/features/providers/config.tsx
  • src/frontend/src/features/ui/components/contact-chip/_index.scss
  • src/frontend/src/features/utils/attachment-helper/index.ts
  • src/frontend/src/features/utils/mail-helper.tsx
  • src/frontend/src/styles/main.scss
📝 Walkthrough

Walkthrough

This PR adds end-to-end inline attachment preview with server-side MIME validation and security controls. The backend implements a preview endpoint that detects MIME types using python-magic, validates against an allowlist, and returns refusal codes. The frontend provides a modal viewer with provenance sidebar, integrates preview hooks across attachment surfaces, extracts Drive upload state into a reusable hook, and updates localization and dependencies.

Changes

Attachment Preview Feature

Layer / File(s) Summary
Backend Preview Endpoint and Core Implementation
src/backend/core/api/openapi.json, src/backend/core/api/viewsets/blob.py
OpenAPI schema for GET /api/v1.0/blob/{id}/preview/ documents MIME detection and refusal; _resolve_blob_source helper centralizes blob/message resolution with authorization; preview action implements MIME sniffing, declared-type normalization, PREVIEWABLE_MIME_TYPES allowlist validation, refusal codes (suspicious vs unsupported), severity logging, and inline response with CSP/security headers.
Backend Preview Enums and Constants
src/backend/core/enums.py
Defines PREVIEWABLE_MIME_TYPES frozenset allowlist (PNG, PDF, JPEG, GIF, WebP, MP3, MP4, WebM) with SVG/HEIC exclusions noted; introduces PreviewRefusalCode StrEnum with SUSPICIOUS and UNSUPPORTED members.
Backend Preview Endpoint Tests
src/backend/core/tests/api/test_blob_preview.py
Comprehensive test suite: real magic-byte fixtures; successful previews validating Content-Type, Content-Disposition, CSP, Cache-Control, and body equality; refusal tests for unsupported MIME and suspicious type mismatch; MIME normalization; authorization denial; blob existence concealment; malformed ID and unauthenticated access handling.
Backend Download Validation Test
src/backend/core/tests/api/test_attachments.py
Verifies blob-download endpoint returns HTTP 400 for malformed (non-UUID) pk.
Backend Drive Config and Preview URL Integration
src/backend/core/api/viewsets/config.py, src/backend/messages/settings.py, src/backend/core/tests/api/test_*.py
ConfigView schema adds read-only preview_url to DRIVE object; runtime construction concatenates base_url + DRIVE_CONFIG[preview_url]; Django Base/Test settings add preview_url: "/media/preview/item"; test fixtures updated.
Frontend Attachment Preview Provider and Layout Integration
src/frontend/src/features/providers/attachment-preview.tsx, src/frontend/src/features/layouts/components/main/index.tsx
AttachmentPreviewProvider manages modal state with open/close handlers and override precedence; provider integrated into MainLayout tree with AttachmentPreviewModal mounted.
Frontend Attachment Preview Modal Component
src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx
AttachmentPreviewModal resolves thread attachments (regular + Drive extracted from HTML) with provenance metadata; applies draft overrides; probes non-Drive endpoints for suspicious MIME mismatch; customizes download behavior (Drive new tab, others use kit URL); renders sidebar conditionally.
Frontend Attachment Preview Sidebar Components
src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar.tsx, src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar-drive-action.tsx
AttachmentPreviewSidebar displays file metadata, provenance/sender/timestamp, suspicious warning, and "Show in conversation" button with smooth scroll and message toggle logic; SidebarDriveAction conditionally renders "Open in Drive" link or "Save in Drive" button with localized state-dependent labels.
Frontend Sidebar Styling and Layout
src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss
SCSS for sidebar: container flex layout, section titles, file row with icon/badge/filename, provenance sender block with truncation, reusable labeled fields, warning/draft callout blocks, actions container.
Frontend Attachment Item Preview Integration
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx
Extends AttachmentItemProps with canPreview and onPreview; computes isPreviewable; triggers preview callback; conditionally adds keyboard accessibility and stable anchor id; stops propagation on action clicks.
Frontend Attachment List Previewable Styling
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss
Adds .attachment-item--previewable with pointer cursor and hover/focus styling; adds .attachment-item.thread-view__highlight with animation and elevated z-index.
Frontend Attachment List and Spam Thread Preview Disabling
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/index.tsx
Updates spam-thread banner mentioning both previewing and downloading disabled; wires canPreview prop driven by !selectedThread?.is_spam.
Frontend Attachment Uploader Preview Wiring
src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx
Integrates useAttachmentPreview; builds memoized previewFiles and driveUrlById map; defines handlePreview callback; wires onPreview prop to items.
Frontend Drive Upload Hook Extraction
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/use-drive-upload.ts
New useDriveUpload(blobId) hook manages upload state, caches Drive file ID in store, wires mutation with logoutOn401: false, auto-clears transient states after timeout.
Frontend Drive Upload Button Refactor
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx
Refactors DriveUploadButton to consume useDriveUpload hook instead of managing state inline; removes prior useState/useEffect wiring.
Frontend Attachment Helper and Preview Type Conversion
src/frontend/src/features/utils/attachment-helper/index.ts, src/frontend/src/features/providers/config.tsx
Introduces MimeFileLike type and narrows mime-related helpers; adds toFilePreviewType and driveFileToFilePreviewType for kit FilePreviewType conversion; updates DEFAULT_DRIVE_CONFIG with preview_url default.
Frontend Drive Attachment Picker MIME Inference
src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx
Adds optional mimetype to patched Drive SDK Item; introduces mimeFromName() helper; updates serializeToDriveFile to use item.mimetype or fallback.
Frontend Message Form Styling and Layout
src/frontend/src/features/forms/components/message-form/_index.scss
.attachment-uploader__input enables wrapping and removes small-screen override; adds .drive-attachment-picker with flex-shrink: 0.
Frontend CSS Text Wrapping Updates
src/frontend/src/features/layouts/components/thread-view/components/thread-event/_index.scss, src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss, src/frontend/src/features/ui/components/contact-chip/_index.scss
Replaces word-break: break-word; with overflow-wrap: break-word; for improved wrapping in multiple SCSS files.
Frontend Build Configuration and Global Imports
src/frontend/src/styles/main.scss, src/frontend/i18next.config.ts, src/frontend/eslint.config.mjs
Adds attachment-preview-modal SCSS import; preserves mime.* i18next keys; extends ESLint ignores for pdf.worker.
Frontend Localization and UI Kit Dependency Update
src/frontend/public/locales/common/en-US.json, src/frontend/public/locales/common/fr-FR.json, src/frontend/package.json, src/frontend/src/features/utils/mail-helper.tsx
English and French locales updated with pluralization variants, preview/MIME keys, spam-thread security notice, and label reordering; UI Kit upgraded from 0.20.0 to 0.23.1; mail-helper syntax fix.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • sylvinus
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title mentions 'attachments preview' which is the main feature added, but it's vague about the scope (frontend-only vs full-stack) and uses a generic emoji prefix that doesn't convey technical detail.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread src/backend/core/api/viewsets/blob.py Fixed
Comment thread src/backend/core/api/viewsets/blob.py Fixed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/core/api/openapi.json`:
- Around line 41-64: The OpenAPI operation describing the FilePreview viewer
promises it will refuse unsupported/suspicious MIME types with HTTP 415 but only
declares a 200 response; add a 415 response object to the operation's
"responses" map (alongside the existing "200") describing the refusal (e.g.,
description: "Unsupported media type / preview refused") and optionally include
a JSON error schema or reference to the existing error response schema; update
the operation that uses tag "blob" and path parameter "id" so generated clients
and server stubs will reflect the 415 contract.

In `@src/backend/core/api/viewsets/blob.py`:
- Around line 289-300: The view compares detected_type to the raw declared_type
(from attachment["type"] or blob.content_type) which may include parameters/case
differences; normalize the declared MIME type to a canonical media type (strip
parameters and lowercase, e.g., "type/subtype") before running the
allowlist/suspicious check and before using it for any comparisons—update the
code paths that set declared_type (attachment branch and blob branch using
models.Blob and get_content) so the normalized declared_type is used in the
allowlist comparison and subsequent logic.
- Around line 279-287: The current error handlers in the blob viewset echo
exception messages from utils.get_attachment_from_blob_id and
models.Blob.DoesNotExist back to the caller; change them to return fixed, stable
messages instead (e.g., for the ValueError branch return a 400 with a generic
"invalid blob identifier" message; for the Blob.DoesNotExist branch return a 404
with a generic "blob not found" message), and move the exception details into
internal logging (use the existing logger to log the exception object). Update
the except blocks that reference utils.get_attachment_from_blob_id and
models.Blob.DoesNotExist to stop using str(e) in the Response payload and log
the exception instead while returning the fixed text and appropriate status
constants (status.HTTP_400_BAD_REQUEST and status.HTTP_404_NOT_FOUND).

In `@src/backend/core/mda/draft.py`:
- Line 284: Replace the incorrect eager warning log in the attachment creation
path: change the call to use logger.debug with lazy %-style or comma-separated
formatting and correct tense; e.g., in draft.py where logger.warning(f'Create
{len(new_attachment_ids)} attachments') is used (around the attachment creation
in the function handling new_attachment_ids), update it to logger.debug("Created
%d attachments", len(new_attachment_ids)) so the message is past tense, uses
debug level, and avoids eager interpolation.

In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx`:
- Around line 66-72: The keydown handler on AttachmentItem (handleKeyDown)
currently reacts to Enter/Space even when those keys originate from inner action
controls (download/delete/retry); update handleKeyDown to ignore events coming
from interactive descendants by checking the event target (e.g., using e.target
or (e.nativeEvent as KeyboardEvent).target) and returning early if the target is
an interactive element (button, [role="button"], a, input, textarea, select, or
an element matching your action control selectors). Keep the existing
isPreviewable guard and call triggerPreview() only when the key event originates
from the item container itself.
- Around line 54-59: isPreviewable currently ignores the backend can_preview
flag and treats persisted attachments as previewable; update the logic in the
isPreviewable calculation to also require the backend-provided can_preview
property (e.g., attachment.can_preview or driveFile.can_preview) when
determining previewability, and adjust previewableId resolution accordingly so
previewableId is only set when canPreview is true and the attachment is actually
previewable; modify the isPreviewable constant and the previewableId ternary
branches (referencing isPreviewable, canPreview, isAttachment, isDriveFile,
previewableId, attachment, variant) to gate both attachment and drive file
preview behavior on the backend can_preview flag.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b853589-db3a-4567-a3f1-b8ec1f50dee1

📥 Commits

Reviewing files that changed from the base of the PR and between 550af75 and 7a2299e.

⛔ Files ignored due to path filters (3)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
  • src/frontend/src/features/api/gen/blob/blob.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/attachment.ts is excluded by !**/gen/**
📒 Files selected for processing (18)
  • src/backend/core/api/openapi.json
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets/blob.py
  • src/backend/core/enums.py
  • src/backend/core/mda/draft.py
  • src/backend/core/tests/api/test_blob_preview.py
  • src/frontend/package.json
  • src/frontend/public/locales/common/en-US.json
  • src/frontend/public/locales/common/fr-FR.json
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx
  • src/frontend/src/features/layouts/components/main/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/index.tsx
  • src/frontend/src/features/providers/attachment-preview.tsx
  • src/frontend/src/features/utils/attachment-helper/index.ts

Comment thread src/backend/core/api/openapi.json
Comment thread src/backend/core/api/viewsets/blob.py Outdated
Comment thread src/backend/core/api/viewsets/blob.py Outdated
Comment thread src/backend/core/mda/draft.py Outdated
@jbpenrath jbpenrath force-pushed the feat/attachment-preview branch 2 times, most recently from 68ab7d6 to 9ace6d6 Compare May 26, 2026 17:32
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/core/api/openapi.json`:
- Around line 60-63: The 200 response currently says "No response body" but this
endpoint returns inline blob data; update the OpenAPI response object for the
"200" entry under the relevant operation's "responses" to declare binary content
(e.g., add a "content" map with an appropriate media type like
"application/octet-stream" or the actual MIME and set the schema to
{"type":"string","format":"binary"}). Locate the "responses" -> "200" object in
src/backend/core/api/openapi.json and replace the "description": "No response
body" with a description plus a "content" block that advertises the binary
payload so generated clients treat the response as bytes rather than void.

In
`@src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss`:
- Line 59: Replace the deprecated CSS declaration "word-break: break-word;" with
the modern property "overflow-wrap: break-word;" in the stylesheet
(_index.scss); locate the rule containing the "word-break: break-word;"
declaration and remove or replace it with "overflow-wrap: break-word;"
(optionally keep "word-break: normal;" if explicit word-break behavior is
needed).
- Line 123: Replace the deprecated CSS declaration "word-break: break-word;"
with the modern property by removing that line and adding "overflow-wrap:
break-word;" in the same selector where "word-break: break-word;" currently
appears so the attachment preview modal text uses overflow-wrap for proper
wrapping; ensure no other occurrences of "word-break: break-word" remain in
_index.scss.

In
`@src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx`:
- Around line 199-202: The code opens driveUrl returned by getDriveUrl(file.id)
without validation; parse and validate driveUrl before calling window.open by
using the URL constructor (or equivalent) to ensure it is a well-formed absolute
URL and that url.protocol is either "http:" or "https:" (or your allowed
schemes), reject anything else, and only then call window.open(driveUrl,
"_blank", "noopener,noreferrer"); update the logic in the
attachment-preview-modal where getDriveUrl and window.open are used to perform
this validation and handle invalid/malformed URLs safely (e.g., show an error or
no-op).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4d311607-1478-4775-acdb-64c5df9f9152

📥 Commits

Reviewing files that changed from the base of the PR and between 7a2299e and 68ab7d6.

⛔ Files ignored due to path filters (4)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
  • src/frontend/src/features/api/gen/blob/blob.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/attachment.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/config_retrieve200_driv_e.ts is excluded by !**/gen/**
📒 Files selected for processing (31)
  • src/backend/core/api/openapi.json
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets/blob.py
  • src/backend/core/api/viewsets/config.py
  • src/backend/core/enums.py
  • src/backend/core/mda/draft.py
  • src/backend/core/tests/api/test_blob_preview.py
  • src/backend/core/tests/api/test_config.py
  • src/backend/core/tests/api/test_drive.py
  • src/backend/messages/settings.py
  • src/frontend/i18next.config.ts
  • src/frontend/package.json
  • src/frontend/public/locales/common/en-US.json
  • src/frontend/public/locales/common/fr-FR.json
  • src/frontend/public/pdf.worker.min.mjs
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx
  • src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx
  • src/frontend/src/features/layouts/components/main/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar-drive-action.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/use-drive-upload.ts
  • src/frontend/src/features/providers/attachment-preview.tsx
  • src/frontend/src/features/providers/config.tsx
  • src/frontend/src/features/utils/attachment-helper/index.ts
  • src/frontend/src/styles/main.scss

Comment thread src/backend/core/api/openapi.json
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/backend/core/api/serializers.py (1)

646-674: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

can_preview is now part of attachment payloads but is missing on parsed-attachment responses.

This serializer change extends the attachment contract, but MessageSerializer.get_attachments() still builds parsed attachment dicts without can_preview, creating a schema/runtime mismatch for non-draft messages.

Suggested fix (in MessageSerializer.get_attachments)
                 stripped_attachments.append(
                     {
                         "blobId": f"msg_{instance.id}_{index}",
                         "name": attachment["name"],
                         "size": attachment["size"],
                         "type": attachment["type"],
                         "cid": attachment.get("cid"),
                         "sha256": attachment.get("sha256"),
+                        "can_preview": (
+                            attachment.get("type") in enums.PREVIEWABLE_MIME_TYPES
+                        ),
                     }
                 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/backend/core/api/serializers.py` around lines 646 - 674,
MessageSerializer.get_attachments currently builds parsed attachment dicts but
omits the new can_preview field, causing a schema mismatch; update
get_attachments to include can_preview for each attachment using the same logic
as Attachment serializer (e.g., set can_preview = attachment.content_type in
enums.PREVIEWABLE_MIME_TYPES or call the serializer/helper that computes
get_can_preview), ensuring parsed-attachment responses include the boolean
alongside blobId, name, size, type, sha256, created_at and cid.
♻️ Duplicate comments (3)
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx (1)

66-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ignore bubbled keyboard events from inner action controls.

Line 66 handles Enter/Space even when the key event originates from nested buttons/links, so keyboard actions can unexpectedly open preview.

💡 Suggested fix
 const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
+    if (e.target !== e.currentTarget) return;
     if (!isPreviewable) return;
     if (e.key === "Enter" || e.key === " ") {
         e.preventDefault();
         triggerPreview();
     }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx`
around lines 66 - 72, handleKeyDown is currently triggering preview for
Enter/Space even when the keyboard event bubbles from nested interactive
controls; update handleKeyDown to ignore bubbled events by verifying the event
originated on the container (e.g., compare e.target to e.currentTarget or check
the event.composedPath/closest for interactive elements like button, a, input,
textarea, select, [role="button"]) before handling; keep the existing
isPreviewable and triggerPreview logic but early-return when the origin is an
inner action control so only direct key presses on the container open the
preview.
src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx (1)

199-202: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate Drive URL scheme before window.open.

At Line 199-Line 202, driveUrl is opened directly. Please parse and enforce http/https before opening.

🔒 Suggested hardening
+const toSafeHttpUrl = (value: string): string | null => {
+    try {
+        const parsed = new URL(value, window.location.origin);
+        return parsed.protocol === "http:" || parsed.protocol === "https:"
+            ? parsed.href
+            : null;
+    } catch {
+        return null;
+    }
+};
+
 const handleDownloadFile = (file?: FilePreviewType) => {
     if (!file?.url) return;
@@
     const driveUrl = getDriveUrl(file.id);
     if (driveUrl) {
-        window.open(driveUrl, "_blank", "noopener,noreferrer");
+        const safeDriveUrl = toSafeHttpUrl(driveUrl);
+        if (!safeDriveUrl) return;
+        window.open(safeDriveUrl, "_blank", "noopener,noreferrer");
         return;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx`
around lines 199 - 202, Validate the driveUrl before calling window.open: in the
attachment-preview-modal component where getDriveUrl(file.id) and
window.open(driveUrl, "_blank", ...) are used, parse driveUrl with the URL
constructor inside a try/catch and only call window.open if url.protocol is
"http:" or "https:"; if parsing fails or protocol is not allowed, do not open
the URL and handle it (e.g., log via console/processLogger or show a user-safe
error) to prevent unsafe schemes.
src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss (1)

59-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace deprecated word-break: break-word.

At Line 59 and Line 123, use overflow-wrap: break-word; instead of deprecated word-break: break-word;.

🎨 Suggested fix
 .attachment-preview-sidebar__file-name {
@@
-    word-break: break-word;
+    overflow-wrap: break-word;
@@
 .attachment-preview-sidebar__field-value {
     margin: 0;
-    word-break: break-word;
+    overflow-wrap: break-word;

Also applies to: 123-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss`
at line 59, Replace the deprecated CSS property usage "word-break: break-word;"
with the supported "overflow-wrap: break-word;" at both occurrences in
_index.scss (the two places where "word-break: break-word;" appears, e.g.,
around the styles at lines referenced in the review). Update both instances so
the styles use overflow-wrap: break-word; instead of word-break to avoid the
deprecated property.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/backend/core/api/viewsets/blob.py`:
- Around line 293-299: Blob lookup with models.Blob.objects.get(id=pk) can raise
a ValidationError for malformed (non-UUID) pk and is currently turning into a
500; validate the pk before querying (e.g., attempt to parse it as a UUID with
uuid.UUID(pk) or explicitly catch
django.core.exceptions.ValidationError/ValueError) and return a 400 Response
with a clear client error message instead of falling through to the generic
exception handler; keep the existing permission check
models.Blob.objects.user_can_access(request.user, blob.id) and subsequent
blob.get_content() flow unchanged once pk is validated.

---

Outside diff comments:
In `@src/backend/core/api/serializers.py`:
- Around line 646-674: MessageSerializer.get_attachments currently builds parsed
attachment dicts but omits the new can_preview field, causing a schema mismatch;
update get_attachments to include can_preview for each attachment using the same
logic as Attachment serializer (e.g., set can_preview = attachment.content_type
in enums.PREVIEWABLE_MIME_TYPES or call the serializer/helper that computes
get_can_preview), ensuring parsed-attachment responses include the boolean
alongside blobId, name, size, type, sha256, created_at and cid.

---

Duplicate comments:
In
`@src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss`:
- Line 59: Replace the deprecated CSS property usage "word-break: break-word;"
with the supported "overflow-wrap: break-word;" at both occurrences in
_index.scss (the two places where "word-break: break-word;" appears, e.g.,
around the styles at lines referenced in the review). Update both instances so
the styles use overflow-wrap: break-word; instead of word-break to avoid the
deprecated property.

In
`@src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx`:
- Around line 199-202: Validate the driveUrl before calling window.open: in the
attachment-preview-modal component where getDriveUrl(file.id) and
window.open(driveUrl, "_blank", ...) are used, parse driveUrl with the URL
constructor inside a try/catch and only call window.open if url.protocol is
"http:" or "https:"; if parsing fails or protocol is not allowed, do not open
the URL and handle it (e.g., log via console/processLogger or show a user-safe
error) to prevent unsafe schemes.

In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx`:
- Around line 66-72: handleKeyDown is currently triggering preview for
Enter/Space even when the keyboard event bubbles from nested interactive
controls; update handleKeyDown to ignore bubbled events by verifying the event
originated on the container (e.g., compare e.target to e.currentTarget or check
the event.composedPath/closest for interactive elements like button, a, input,
textarea, select, [role="button"]) before handling; keep the existing
isPreviewable and triggerPreview logic but early-return when the origin is an
inner action control so only direct key presses on the container open the
preview.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 26ef047b-b02f-43ac-adb6-977f41170957

📥 Commits

Reviewing files that changed from the base of the PR and between 68ab7d6 and 9ace6d6.

⛔ Files ignored due to path filters (4)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
  • src/frontend/src/features/api/gen/blob/blob.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/attachment.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/config_retrieve200_driv_e.ts is excluded by !**/gen/**
📒 Files selected for processing (31)
  • src/backend/core/api/openapi.json
  • src/backend/core/api/serializers.py
  • src/backend/core/api/viewsets/blob.py
  • src/backend/core/api/viewsets/config.py
  • src/backend/core/enums.py
  • src/backend/core/tests/api/test_blob_preview.py
  • src/backend/core/tests/api/test_config.py
  • src/backend/core/tests/api/test_drive.py
  • src/backend/messages/settings.py
  • src/frontend/eslint.config.mjs
  • src/frontend/i18next.config.ts
  • src/frontend/package.json
  • src/frontend/public/locales/common/en-US.json
  • src/frontend/public/locales/common/fr-FR.json
  • src/frontend/public/pdf.worker.min.mjs
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx
  • src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx
  • src/frontend/src/features/layouts/components/main/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar-drive-action.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/use-drive-upload.ts
  • src/frontend/src/features/providers/attachment-preview.tsx
  • src/frontend/src/features/providers/config.tsx
  • src/frontend/src/features/utils/attachment-helper/index.ts
  • src/frontend/src/styles/main.scss

Comment thread src/backend/core/api/viewsets/blob.py Outdated
@jbpenrath jbpenrath force-pushed the feat/attachment-preview branch 2 times, most recently from 66df5e6 to b2b3d50 Compare May 27, 2026 12:04
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx (1)

75-95: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix stale closure in pick callback dependencies.

pick reads config.DRIVE!.sdk_url / config.DRIVE!.api_url and calls the onPick prop, but useCallback only depends on isDriveDisabled, so it can use stale config/onPick after updates.

Proposed fix
-    }, [isDriveDisabled]);
+    }, [config.DRIVE?.api_url, config.DRIVE?.sdk_url, isDriveDisabled, onPick]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx`
around lines 75 - 95, The pick callback has a stale-closure bug because
useCallback only depends on isDriveDisabled while it reads config.DRIVE!.sdk_url
/ config.DRIVE!.api_url and calls the onPick prop; update the dependency array
for the useCallback around pick to include the values it reads (either config or
the specific config.DRIVE fields and onPick) so pick always captures the latest
config and onPick, or alternatively memoize the sdk_url/api_url and onPick
before creating the callback; locate the pick function, the useCallback
invocation, and references to config.DRIVE and onPick to apply the change.
♻️ Duplicate comments (2)
src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx (2)

66-72: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Ignore bubbled keyboard events from inner action controls.

Line 66 currently reacts to Enter/Space even when focus is inside nested action buttons/links, so keyboard actions can also open preview unintentionally.

💡 Suggested fix
     const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
+        if (e.target !== e.currentTarget) return;
         if (!isPreviewable) return;
         if (e.key === "Enter" || e.key === " ") {
             e.preventDefault();
             triggerPreview();
         }
     };

Also applies to: 112-112

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx`
around lines 66 - 72, handleKeyDown is opening the preview when Enter/Space
bubbles up from inner action controls; modify handleKeyDown so it ignores events
originating inside nested interactive elements by checking the event target
before triggering preview (e.g., ensure e.target is the container itself or that
(e.target as HTMLElement).closest('button, a, [role="button"],
[data-attachment-action]') is null) and only call triggerPreview when that check
passes; apply the same guard to the identical handler at the other occurrence
referenced (line ~112).

54-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gate previewability with backend capability flag.

Line 54 still allows opening preview for persisted attachments that the API marks as non-previewable (can_preview: false), which can open a modal for unsupported files.

💡 Suggested fix
-    const isPreviewable = canPreview && (isAttachment(attachment) || isDriveFile(attachment)) && variant !== "error";
+    const isServerPreviewable = isAttachment(attachment) ? attachment.can_preview : true;
+    const isPreviewable =
+        canPreview &&
+        isServerPreviewable &&
+        (isAttachment(attachment) || isDriveFile(attachment)) &&
+        variant !== "error";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx`
around lines 54 - 59, The preview logic allows opening previews for files the
backend flagged as non-previewable; update the isPreviewable and previewableId
computation to also require the attachment's backend flag (can_preview) to be
true (i.e., guard with attachment.can_preview !== false or equivalent) for both
Attachment and DriveFile cases, keeping the existing canPreview and variant !==
"error" checks and only returning a previewableId when that flag permits
previewing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/frontend/public/locales/common/en-US.json`:
- Around line 707-708: Replace the generic message "The email address is
invalid." with the parameterized string "The email {{email}} is invalid." so the
translation preserves the {{email}} template variable; update the JSON entry so
the key and value include "{{email}}" (i.e., restore the original "The email
{{email}} is invalid." string) to provide specific feedback in multi-recipient
contexts.

In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/use-drive-upload.ts`:
- Around line 15-19: The hook useDriveUpload initializes driveFileId only once
from driveUploadStore.get(blobId), so when the incoming blobId changes the hook
retains the old driveFileId/state; add an effect that watches blobId and resets
the cached values—call setDriveFileId(driveUploadStore.get(blobId)) and reset
state (e.g. setState("idle") or appropriate initial DriveUploadState) inside a
useEffect with [blobId] so the hook reflects the new blobId; reference
useDriveUpload, driveFileId, driveUploadStore, blobId, setDriveFileId and
setState to locate where to add the effect.

---

Outside diff comments:
In
`@src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx`:
- Around line 75-95: The pick callback has a stale-closure bug because
useCallback only depends on isDriveDisabled while it reads config.DRIVE!.sdk_url
/ config.DRIVE!.api_url and calls the onPick prop; update the dependency array
for the useCallback around pick to include the values it reads (either config or
the specific config.DRIVE fields and onPick) so pick always captures the latest
config and onPick, or alternatively memoize the sdk_url/api_url and onPick
before creating the callback; locate the pick function, the useCallback
invocation, and references to config.DRIVE and onPick to apply the change.

---

Duplicate comments:
In
`@src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx`:
- Around line 66-72: handleKeyDown is opening the preview when Enter/Space
bubbles up from inner action controls; modify handleKeyDown so it ignores events
originating inside nested interactive elements by checking the event target
before triggering preview (e.g., ensure e.target is the container itself or that
(e.target as HTMLElement).closest('button, a, [role="button"],
[data-attachment-action]') is null) and only call triggerPreview when that check
passes; apply the same guard to the identical handler at the other occurrence
referenced (line ~112).
- Around line 54-59: The preview logic allows opening previews for files the
backend flagged as non-previewable; update the isPreviewable and previewableId
computation to also require the attachment's backend flag (can_preview) to be
true (i.e., guard with attachment.can_preview !== false or equivalent) for both
Attachment and DriveFile cases, keeping the existing canPreview and variant !==
"error" checks and only returning a previewableId when that flag permits
previewing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d0ed39e0-a73a-4cc1-822a-b5f6c99014b5

📥 Commits

Reviewing files that changed from the base of the PR and between 9ace6d6 and b2b3d50.

⛔ Files ignored due to path filters (3)
  • src/frontend/package-lock.json is excluded by !**/package-lock.json
  • src/frontend/src/features/api/gen/blob/blob.ts is excluded by !**/gen/**
  • src/frontend/src/features/api/gen/models/config_retrieve200_driv_e.ts is excluded by !**/gen/**
📒 Files selected for processing (36)
  • src/backend/core/api/openapi.json
  • src/backend/core/api/viewsets/blob.py
  • src/backend/core/api/viewsets/config.py
  • src/backend/core/enums.py
  • src/backend/core/tests/api/test_attachments.py
  • src/backend/core/tests/api/test_blob_preview.py
  • src/backend/core/tests/api/test_config.py
  • src/backend/core/tests/api/test_drive.py
  • src/backend/messages/settings.py
  • src/frontend/eslint.config.mjs
  • src/frontend/i18next.config.ts
  • src/frontend/package.json
  • src/frontend/public/locales/common/en-US.json
  • src/frontend/public/locales/common/fr-FR.json
  • src/frontend/public/pdf.worker.min.mjs
  • src/frontend/src/features/forms/components/message-form/_index.scss
  • src/frontend/src/features/forms/components/message-form/attachment-uploader.tsx
  • src/frontend/src/features/forms/components/message-form/drive-attachment-picker.tsx
  • src/frontend/src/features/layouts/components/main/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar-drive-action.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/attachment-preview-modal/sidebar.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/attachment-item.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/drive-upload-button.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/index.tsx
  • src/frontend/src/features/layouts/components/thread-view/components/thread-attachment-list/use-drive-upload.ts
  • src/frontend/src/features/layouts/components/thread-view/components/thread-event/_index.scss
  • src/frontend/src/features/layouts/components/thread-view/components/thread-message/_index.scss
  • src/frontend/src/features/providers/attachment-preview.tsx
  • src/frontend/src/features/providers/config.tsx
  • src/frontend/src/features/ui/components/contact-chip/_index.scss
  • src/frontend/src/features/utils/attachment-helper/index.ts
  • src/frontend/src/features/utils/mail-helper.tsx
  • src/frontend/src/styles/main.scss

Comment thread src/frontend/public/locales/common/en-US.json
Use the Preview component to allow user to
preview message attachment from a thread.
@jbpenrath jbpenrath force-pushed the feat/attachment-preview branch from b2b3d50 to 6834740 Compare May 27, 2026 13:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants