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

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

Feature: mentions bar with conversation partners (klonkt-demo-u02)

Implicit mentions leave the text and become an editable "To:" bar above the
reply editor: the parent author plus the thread's ancestor authors as chips.
Removing a chip stops addressing that partner (no mention anchor, no Mention
tag, no inbox ping); explicit @mentions typed in the text keep working via the
existing resolveMentionsInText path.

  • getInteractions: each thread node gets "participants" (author + ancestor chain via the byId map, own nodes skipped, deduped, capped at 8) and carries actor_uri now.
  • reply-editor partial: the bar renders server-side with a hidden "mentions" JSON field carrying the full list, so the no-JS path addresses everyone; the JS removes chips and mirrors the remaining list into the field.
  • deliverReply({mentions}): undefined = legacy parent-only behavior; an array (possibly empty) = the kept list drives the mention prefix, the Mention tags (mentionTags reads the content anchors) and the per-actor inbox pings. to_actor/to_handle follow the kept list (parent when kept, else the first chip, else null -> the note addresses Public only).
  • deliverOutboxUpdate: an edit reuses the OLD content's leading mention anchors instead of rebuilding just to_actor, so co-mentions survive edits (legacy rows fall back as before).
  • Interact page passes the target author as the single chip. Full-screen mobile keeps the top bar above the mentions bar (flex order).

4 new tests (participant chains, multi-chip tags + to_actor, empty bar goes
Public-only, co-mentions survive edits); 97 green. Browser-verified: bar shows
@bob + @alice on a nested reply, removing @alice updates the hidden field, the
sent reply mentions and addresses only @bob; no console errors.

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

  • Property mode set to 100644
File size: 5.6 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)
[5190152]18 initialHtml prefill for EDIT mode (our own sanitized HTML) (optional)
19 initialText plain-text prefill for the no-JS textarea (optional)
20 noAttach true hides the media button (edits do not touch attachments)
[e9c9ae1]21 participants [{uri, url, handle}] conversation partners → the mentions bar
22 (u02): implicit mentions live here as removable chips, not in
23 the text. Absent = no bar, legacy addressing.
[33e1dbd]24%>
[5190152]25<% var _reInitHtml = (typeof initialHtml !== 'undefined' && initialHtml) || ''; %>
26<% var _reInitText = (typeof initialText !== 'undefined' && initialText) || ''; %>
27<% var _reNoAttach = (typeof noAttach !== 'undefined' && noAttach); %>
[e9c9ae1]28<% var _reParts = (typeof participants !== 'undefined' && participants && participants.length) ? participants : null; %>
[33e1dbd]29<% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %>
[feced2c]30<form method="post" action="<%= action %>" class="re-form" data-re
31 data-upload="/posts/upload-reply-media" data-upload-err="<%= t('re.attach_err') %>">
[33e1dbd]32 <% (typeof hiddenFields !== 'undefined' ? hiddenFields : []).forEach(function (h) { %>
33 <input type="hidden" name="<%= h.name %>" value="<%= h.value %>">
34 <% }); %>
35 <input type="hidden" name="content" value="">
[e9c9ae1]36 <%# Mentions bar (u02): the conversation partners as removable chips. The
37 hidden field always carries the CURRENT kept list (server-rendered full,
38 so the no-JS path addresses everyone). %>
39 <% if (_reParts) { %>
40 <input type="hidden" name="mentions" value="<%= JSON.stringify(_reParts) %>">
41 <div class="re-mentions">
42 <span class="re-mentions-label"><%= t('re.to') %></span>
43 <% _reParts.forEach(function (p) { %>
44 <span class="re-mention" data-uri="<%= p.uri %>"><%= p.handle && p.handle[0] === '@' ? p.handle : '@' + p.handle %><button type="button" class="re-mention-del" title="<%= t('re.mention_del') %>" aria-label="<%= t('re.mention_del') %>">×</button></span>
45 <% }); %>
46 </div>
47 <% } %>
[33e1dbd]48 <%# Full-screen top bar (mobile compose). Hidden until JS + fullscreen. %>
49 <div class="re-head" hidden>
50 <button type="button" class="re-cancel" aria-label="<%= t('comments.cancel') %>">×</button>
51 <strong class="re-head-title"><%= t('re.title') %></strong>
52 <button type="submit" class="btn re-send-top"><%= submitLabel %></button>
53 </div>
54 <div class="re-toolbar" role="toolbar" aria-label="<%= t('re.title') %>" hidden>
55 <button type="button" data-cmd="bold" title="<%= t('re.bold') %>" aria-label="<%= t('re.bold') %>"><b>B</b></button>
56 <button type="button" data-cmd="italic" title="<%= t('re.italic') %>" aria-label="<%= t('re.italic') %>"><i>I</i></button>
57 <button type="button" data-cmd="link" title="<%= t('re.link') %>" aria-label="<%= t('re.link') %>">🔗</button>
58 <button type="button" data-cmd="list" title="<%= t('re.list') %>" aria-label="<%= t('re.list') %>">•≡</button>
59 <button type="button" data-cmd="quote" title="<%= t('re.quote') %>" aria-label="<%= t('re.quote') %>">❝</button>
[5190152]60 <% if (!_reNoAttach) { %>
61 <button type="button" data-cmd="attach" title="<%= t('re.attach') %>" aria-label="<%= t('re.attach') %>">📎</button>
62 <input type="file" class="re-file" accept="image/*,audio/*,video/*" multiple hidden>
63 <% } %>
[33e1dbd]64 </div>
[5190152]65 <% if (!_reNoAttach) { %>
66 <input type="hidden" name="attachments" value="">
67 <div class="re-attachments" hidden></div>
68 <% } %>
69 <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"><%= _reInitText %></textarea>
[33e1dbd]70 <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true"
[5190152]71 data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden><%- _reInitHtml %></div>
[33e1dbd]72 <div class="re-foot">
73 <select name="language" class="re-lang" aria-label="<%= t('re.lang') %>" hidden>
74 <% [['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) { %>
75 <option value="<%= l[0] %>" <%= l[0] === _reLang ? 'selected' : '' %>><%= l[1] %></option>
76 <% }); %>
77 </select>
78 <button type="submit" class="btn re-send"><%= submitLabel %></button>
79 </div>
80</form>
81<%# Assets once per render, even when this partial repeats per comment. %>
82<% if (!locals.__reAssets) { locals.__reAssets = 1; %>
83<link rel="stylesheet" href="/assets/css/reply-editor.css">
84<script src="/assets/js/reply-editor.js" defer></script>
85<% } %>
Note: See TracBrowser for help on using the repository browser.