attachments preview overlay, docx/xlsx/odf rendering, composer drag-drop & undo-send UX
Frontend: - Full-screen attachment preview overlay (AttachmentPreview) for image/pdf/txt plus office docs: .docx via docx-preview (rendered live into the sandboxed iframe so header/footer VML images survive), .xlsx via xlsx-preview, and .odt/.ods/.odp/.odg via a JSZip+DOMParser ODF walker (officeConvert). - Type-aware attachment icons (green spreadsheet, blue document, red PDF, image/archive); new document/image/spreadsheet/print SVGs. - Composer: drag-drop attachments (body drop inserts images inline, else attaches); inserted images are selectable/resizable/deletable (RichTextEditor). - Undo-send: "Sending…" banner with live "N seconds to Undo" countdown. - Search result list: pin/archive/delete update correctly; grey-out only for search hits; scoped "Search in Trash/Spam". Backend: - Search: prefix (search-as-you-type) matching, email-address search, and address-first result ordering; Trash/Junk excluded with scoped folder search. - Attachments: RFC 6266 Content-Disposition for non-ASCII filenames; one-time inline image loading. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rcj3TpydMTzTSmU7kunzf8
This commit is contained in:
@@ -7,6 +7,7 @@ import io
|
||||
import uuid
|
||||
import zipfile
|
||||
from typing import Annotated
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi.responses import HTMLResponse, Response
|
||||
@@ -146,6 +147,15 @@ async def message_cid(
|
||||
return Response(content=data, media_type=media_type)
|
||||
|
||||
|
||||
def _content_disposition(disposition: str, filename: str) -> str:
|
||||
"""RFC 6266 Content-Disposition safe for non-ASCII names. HTTP header values
|
||||
are Latin-1, so a raw Polish/CJK filename (ł, ń, …) crashes the response with
|
||||
a 500; emit an ASCII fallback plus a UTF-8 ``filename*`` for modern clients."""
|
||||
ascii_name = filename.encode("ascii", "ignore").decode("ascii").replace('"', "").strip()
|
||||
fallback = ascii_name or "attachment"
|
||||
return f"{disposition}; filename=\"{fallback}\"; filename*=UTF-8''{quote(filename)}"
|
||||
|
||||
|
||||
async def _real_attachments(db: AsyncSession, message_id: uuid.UUID) -> list[Attachment]:
|
||||
"""Downloadable (non-inline) attachments for a message, in a stable order."""
|
||||
rows = await db.scalars(
|
||||
@@ -234,5 +244,5 @@ async def message_attachment(
|
||||
return Response(
|
||||
content=data,
|
||||
media_type=media_type,
|
||||
headers={"Content-Disposition": f'{disposition}; filename="{att.filename}"'},
|
||||
headers={"Content-Disposition": _content_disposition(disposition, att.filename)},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user