source: Klonkt/src/views/partials/reply-editor.ejs@ feced2c

main
Last change on this file since feced2c was feced2c, checked in by Robin <roboburr@…>, 7 weeks ago

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@…>

  • Property mode set to 100644
File size: 3.9 KB
RevLine 
[33e1dbd]1<%#
2 Rich reply editor (shared component) — progressive enhancement.
3
4 Renders a plain <textarea name="text"> form that works without JS; the shared
5 /assets/js/reply-editor.js upgrades it to a rich contenteditable with a small
6 toolbar, a language select and (on ≤700px) a full-screen compose overlay,
7 which is the right pattern on mobile. On submit the JS fills the hidden
8 `content` field with the editor HTML and `text` with a plain-text fallback;
9 the server prefers `content` and sanitizes it (fedi-reply / authorize_interaction).
10
11 Locals:
12 action POST target (required)
13 hiddenFields [{ name, value }] extra hidden inputs (optional)
14 placeholder textarea/editor placeholder (required)
15 submitLabel submit button label (required)
16 rows textarea rows for the no-JS fallback (default 3)
17 defaultLang preselected reply language (default: UI lang)
18%>
19<% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %>
[feced2c]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') %>">
[33e1dbd]22 <% (typeof hiddenFields !== 'undefined' ? hiddenFields : []).forEach(function (h) { %>
23 <input type="hidden" name="<%= h.name %>" value="<%= h.value %>">
24 <% }); %>
25 <input type="hidden" name="content" value="">
26 <%# Full-screen top bar (mobile compose). Hidden until JS + fullscreen. %>
27 <div class="re-head" hidden>
28 <button type="button" class="re-cancel" aria-label="<%= t('comments.cancel') %>">×</button>
29 <strong class="re-head-title"><%= t('re.title') %></strong>
30 <button type="submit" class="btn re-send-top"><%= submitLabel %></button>
31 </div>
32 <div class="re-toolbar" role="toolbar" aria-label="<%= t('re.title') %>" hidden>
33 <button type="button" data-cmd="bold" title="<%= t('re.bold') %>" aria-label="<%= t('re.bold') %>"><b>B</b></button>
34 <button type="button" data-cmd="italic" title="<%= t('re.italic') %>" aria-label="<%= t('re.italic') %>"><i>I</i></button>
35 <button type="button" data-cmd="link" title="<%= t('re.link') %>" aria-label="<%= t('re.link') %>">🔗</button>
36 <button type="button" data-cmd="list" title="<%= t('re.list') %>" aria-label="<%= t('re.list') %>">•≡</button>
37 <button type="button" data-cmd="quote" title="<%= t('re.quote') %>" aria-label="<%= t('re.quote') %>">❝</button>
[feced2c]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>
[33e1dbd]40 </div>
[feced2c]41 <input type="hidden" name="attachments" value="">
42 <div class="re-attachments" hidden></div>
[33e1dbd]43 <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"></textarea>
44 <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true"
45 data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden></div>
46 <div class="re-foot">
47 <select name="language" class="re-lang" aria-label="<%= t('re.lang') %>" hidden>
48 <% [['en','English'],['nl','Nederlands'],['de','Deutsch'],['fr','Français'],['es','Español'],['it','Italiano'],['pt','Português'],['pl','Polski'],['ru','Русский'],['uk','Українська'],['sv','Svenska'],['da','Dansk'],['nb','Norsk'],['fi','Suomi'],['tr','Türkçe'],['ja','日本語'],['zh','中文'],['ar','العربية']].forEach(function (l) { %>
49 <option value="<%= l[0] %>" <%= l[0] === _reLang ? 'selected' : '' %>><%= l[1] %></option>
50 <% }); %>
51 </select>
52 <button type="submit" class="btn re-send"><%= submitLabel %></button>
53 </div>
54</form>
55<%# Assets once per render, even when this partial repeats per comment. %>
56<% if (!locals.__reAssets) { locals.__reAssets = 1; %>
57<link rel="stylesheet" href="/assets/css/reply-editor.css">
58<script src="/assets/js/reply-editor.js" defer></script>
59<% } %>
Note: See TracBrowser for help on using the repository browser.