Ignore:
Timestamp:
07/19/2026 05:25:26 PM (7 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
5190152
Parents:
33e1dbd
git-author:
Robin <roboburr@…> (07/19/2026 05:24:58 PM)
git-committer:
Robin <roboburr@…> (07/19/2026 05:25:26 PM)
Message:

Feature: media in replies — rich replies phase 2 (klonkt-demo-c7f)

Drop, paste or pick images/audio/video in the reply editor; they upload, show
as removable chips, travel as AS2 attachments on the federated Note, and render
in the thread.

  • POST /posts/upload-reply-media (requireSiteManager): image/audio/video by extension AND mimetype, stored as-is under /media/reply-media/ (no transcode; a reply attachment is not a track), 32MB cap, returns {url, mediaType, name}.
  • Editor: paperclip button + hidden file input (the mobile path), paste-files and drag/drop handlers, busy/error chips, image thumbnails, max 4, hidden attachments JSON field. Media-only submit allowed (text no longer required when something is attached).
  • deliverReply({attachments}): re-validates server-side — own /media/ paths only (the upload route is the sole producer, remote URLs rejected), image|audio|video mimetypes, capped at 4; stored as JSON on ap_outbox (additive column). Dedup guard now includes attachments so two media-only replies to the same parent are distinct from each other but double-submits still dedup.
  • buildNote reply branch: attachment array with Image/Audio/Video types and absolute URLs. getInteractions passes media through; fedi-node renders it (img/audio/video) for visitors too, loading the stylesheet when the owner-only editor is not on the page.

3 new tests (foreign-URL and type rejection, typed absolute Note attachments,
media-only allowed, only-invalid rejected); 91 green. Browser-verified end to
end: real upload via the endpoint, paste-event -> chip with thumbnail ->
submit -> ap_outbox row with content+language+attachments -> media rendered in
the thread -> /ap/notes/<id> serves the typed absolute attachment.

Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • src/views/partials/reply-editor.ejs

    r33e1dbd rfeced2c  
    1818%>
    1919<% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %>
    20 <form method="post" action="<%= action %>" class="re-form" data-re>
     20<form method="post" action="<%= action %>" class="re-form" data-re
     21      data-upload="/posts/upload-reply-media" data-upload-err="<%= t('re.attach_err') %>">
    2122  <% (typeof hiddenFields !== 'undefined' ? hiddenFields : []).forEach(function (h) { %>
    2223    <input type="hidden" name="<%= h.name %>" value="<%= h.value %>">
     
    3536    <button type="button" data-cmd="list" title="<%= t('re.list') %>" aria-label="<%= t('re.list') %>">•≡</button>
    3637    <button type="button" data-cmd="quote" title="<%= t('re.quote') %>" aria-label="<%= t('re.quote') %>">❝</button>
     38    <button type="button" data-cmd="attach" title="<%= t('re.attach') %>" aria-label="<%= t('re.attach') %>">📎</button>
     39    <input type="file" class="re-file" accept="image/*,audio/*,video/*" multiple hidden>
    3740  </div>
     41  <input type="hidden" name="attachments" value="">
     42  <div class="re-attachments" hidden></div>
    3843  <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"></textarea>
    3944  <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true"
Note: See TracChangeset for help on using the changeset viewer.