Changeset 5190152 in Klonkt
- Timestamp:
- 07/19/2026 05:38:25 PM (7 weeks ago)
- Branches:
- main
- Children:
- e9c9ae1
- Parents:
- feced2c
- git-author:
- Robin <roboburr@…> (07/19/2026 05:38:00 PM)
- git-committer:
- Robin <roboburr@…> (07/19/2026 05:38:25 PM)
- Files:
-
- 10 edited
-
CHANGELOG.de.md (modified) (1 diff)
-
CHANGELOG.md (modified) (1 diff)
-
CHANGELOG.nl.md (modified) (1 diff)
-
src/assets/js/reply-editor.js (modified) (7 diffs)
-
src/routes/posts.js (modified) (1 diff)
-
src/services/ActivityPubService.js (modified) (4 diffs)
-
src/views/pages/authorize-interaction.ejs (modified) (2 diffs)
-
src/views/pages/messages.ejs (modified) (1 diff)
-
src/views/partials/reply-editor.ejs (modified) (2 diffs)
-
test/rich-reply.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
CHANGELOG.de.md
rfeced2c r5190152 7 7 8 8 ### Hinzugefügt 9 - **Das Bearbeiten einer gesendeten Antwort nutzt jetzt ebenfalls den 10 Rich-Editor.** Die Bearbeiten-Formulare auf Nachrichten und der 11 Interaktionsseite öffnen denselben Editor wie neue Antworten (Formatierung 12 bleibt, Sprache anpassbar, Vollbild auf Telefonen). Anhänge der Antwort 13 überstehen eine Bearbeitung unangetastet. 9 14 - **Medien in Antworten (reichhaltige Antworten, Phase 2).** Ziehe, füge ein 10 15 oder wähle Bilder, Audio und Video direkt im Antwort-Editor (die Büroklammer -
CHANGELOG.md
rfeced2c r5190152 7 7 8 8 ### Added 9 - **Editing a sent reply uses the rich editor too.** The edit forms on Messages 10 and the interact page open the same editor as new replies (formatting kept, 11 language adjustable, full-screen on phones). Attachments on the reply survive 12 an edit untouched. 9 13 - **Media in replies (rich replies, phase 2).** Drop, paste or pick images, 10 14 audio and video straight into the reply editor (the paperclip works on -
CHANGELOG.nl.md
rfeced2c r5190152 7 7 8 8 ### Toegevoegd 9 - **Een verzonden reactie bewerken gebruikt nu ook de rijke editor.** De 10 bewerk-formulieren op Berichten en de interactiepagina openen dezelfde editor 11 als nieuwe reacties (opmaak blijft, taal aanpasbaar, volledig scherm op 12 telefoons). Bijlagen op de reactie overleven een bewerking onaangeroerd. 9 13 - **Media in reacties (rijke reacties, fase 2).** Sleep, plak of kies 10 14 afbeeldingen, audio en video direct in de reactie-editor (de paperclip werkt -
src/assets/js/reply-editor.js
rfeced2c r5190152 36 36 var attField = form.querySelector('input[name="attachments"]'); 37 37 var fileInput = form.querySelector('.re-file'); 38 var canAttach = !!(attWrap && attField && fileInput); // edit mode renders without media 38 39 var attachments = []; 39 40 40 41 function syncAtt() { 42 if (!canAttach) return; 41 43 attField.value = attachments.length ? JSON.stringify(attachments) : ''; 42 44 attWrap.hidden = attachments.length === 0; … … 62 64 } 63 65 function uploadFiles(files) { 66 if (!canAttach) return; 64 67 Array.prototype.forEach.call(files, function (file) { 65 68 if (!/^(image|audio|video)\//.test(file.type) || attachments.length >= 4) return; … … 93 96 e.preventDefault(); 94 97 var cmd = btn.getAttribute('data-cmd'); 95 if (cmd === 'attach') { fileInput.click(); return; } // no editor focus: keeps the picker usable on mobile98 if (cmd === 'attach') { if (fileInput) fileInput.click(); return; } // no editor focus: keeps the picker usable on mobile 96 99 ed.focus(); 97 100 if (cmd === 'bold') document.execCommand('bold'); … … 104 107 } 105 108 }); 106 fileInput.addEventListener('change', function () {109 if (fileInput) fileInput.addEventListener('change', function () { 107 110 uploadFiles(fileInput.files); 108 111 fileInput.value = ''; … … 114 117 var cd = e.clipboardData || window.clipboardData; 115 118 if (cd.files && cd.files.length) { 116 e.preventDefault(); 117 uploadFiles(cd.files); 119 e.preventDefault(); // never let the browser inline-paste a file as base64 120 uploadFiles(cd.files); // no-op without the attach UI (edit mode) 118 121 return; 119 122 } … … 125 128 126 129 // Drag/drop media onto the editor. 130 if (canAttach) { 127 131 ed.addEventListener('dragover', function (e) { 128 132 if (e.dataTransfer && Array.prototype.some.call(e.dataTransfer.types || [], function (t) { return t === 'Files'; })) { … … 139 143 } 140 144 }); 145 } 141 146 142 147 // Full-screen compose on mobile: enter on focus, leave via ×. -
src/routes/posts.js
rfeced2c r5190152 827 827 router.post('/fediverse/:id/edit', requireSiteManager, async (req, res) => { 828 828 const site = res.locals.site; 829 if (site && String(req.body.text || '').trim()) { 830 try { await ActivityPubService.deliverOutboxUpdate(site, req.params.id, req.body.text); } 831 catch (e) { console.warn('[AP] outbox edit failed:', e.message); } 829 const text = String(req.body.text || ''); 830 const html = String(req.body.content || ''); // rich reply editor HTML (sanitized in deliverOutboxUpdate) 831 if (site && (text.trim() || html.trim())) { 832 try { 833 await ActivityPubService.deliverOutboxUpdate(site, req.params.id, text, { 834 html, language: String(req.body.language || ''), 835 }); 836 } catch (e) { console.warn('[AP] outbox edit failed:', e.message); } 832 837 } 833 838 res.redirect(req.get('Referer') || `${res.locals.siteUrlBase || ''}/fediverse`); -
src/services/ActivityPubService.js
rfeced2c r5190152 513 513 items.push({ 514 514 type: 'sent', outboxId: m.id, to_handle: m.to_handle, in_reply_to: m.in_reply_to, 515 content: m.content, editable: m.editable, created_at: m.created_at,515 content: m.content, editable: m.editable, language: m.language, created_at: m.created_at, 516 516 }); 517 517 } … … 2049 2049 } 2050 2050 export function listOutbox(siteSlug) { 2051 return db.prepare('SELECT id, content, to_handle, in_reply_to, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')2051 return db.prepare('SELECT id, content, to_handle, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC') 2052 2052 .all(siteSlug).map((r) => { const c = stripLeadingMentions(r.content); return { ...r, content: c, editable: outboxEditableText(c) }; }); 2053 2053 } … … 2077 2077 // Edit one of our outbound replies: rewrite the stored content (mention re-added + #tags 2078 2078 // re-linked) and send an Update(Note) so recipients refresh their cached copy. 2079 export async function deliverOutboxUpdate(site, outboxId, newText ) {2079 export async function deliverOutboxUpdate(site, outboxId, newText, opts = {}) { 2080 2080 const row = iStmts().getO.get(outboxId); 2081 2081 if (!row || row.site_slug !== site.slug) return false; 2082 2082 const text = String(newText || '').trim(); 2083 if (!text) return false; 2083 // Rich edit: same sanitize + enrichment pipeline as deliverReply. 2084 const richClean = opts.html ? HtmlSanitizerService.sanitize(String(opts.html)) : ''; 2085 const rich = richClean && HtmlSanitizerService.toPlainText(richClean).trim() ? richClean : ''; 2086 if (!text && !rich) return false; 2084 2087 const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, ''); 2085 2088 if (!base) return false; … … 2091 2094 const mention = row.to_actor 2092 2095 ? `<a href="${escHtml(toProfile)}" class="u-url mention" data-actor="${escHtml(row.to_actor)}">${escHtml(toHandle)}</a> ` : ''; 2093 const mres = await resolveMentionsInText(base, escHtml(text).replace(/\r?\n/g, '<br>')); 2094 const content = `<p>${mention}${linkUrls(linkHashtags(base, mres.html))}</p>`; 2095 db.prepare('UPDATE ap_outbox SET content = ? WHERE id = ?').run(content, outboxId); 2096 let content; 2097 let mres; 2098 if (rich) { 2099 mres = await resolveMentionsInText(base, rich); 2100 const processed = linkUrls(linkHashtags(base, mres.html)); 2101 if (processed.startsWith('<p>')) content = processed.replace('<p>', `<p>${mention}`); 2102 else if (/^<(blockquote|ul|ol|pre|h[1-6]|div|hr)\b/i.test(processed)) content = `<p>${mention}</p>${processed}`; 2103 else content = `<p>${mention}${processed}</p>`; 2104 } else { 2105 mres = await resolveMentionsInText(base, escHtml(text).replace(/\r?\n/g, '<br>')); 2106 content = `<p>${mention}${linkUrls(linkHashtags(base, mres.html))}</p>`; 2107 } 2108 // Language may be updated with the edit; attachments always survive untouched. 2109 const newLang = /^[a-z]{2,3}(-[A-Za-z0-9-]+)?$/.test(String(opts.language || '')) ? opts.language : null; 2110 db.prepare('UPDATE ap_outbox SET content = ?, language = COALESCE(?, language) WHERE id = ?').run(content, newLang, outboxId); 2096 2111 const note = buildReplyNote(base, site, iStmts().getO.get(outboxId)); 2097 2112 note.updated = new Date().toISOString(); -
src/views/pages/authorize-interaction.ejs
rfeced2c r5190152 41 41 </form> 42 42 </div> 43 <form method="post" action="/fediverse/<%= m.id %>/edit" class="fedi-edit-form"> 44 <textarea name="text" rows="3" maxlength="2000" class="fedi-edit-ta"><%= m.editable %></textarea> 45 <div class="fedi-edit-actions"><button type="submit" class="fedi-edit-save"><%= t('fedi.save_edit') %></button></div> 46 </form> 43 <div class="fedi-edit-form"> 44 <%- include('../partials/reply-editor', { 45 action: '/fediverse/' + m.id + '/edit', 46 placeholder: t('fedi.reply_ph'), 47 submitLabel: t('fedi.save_edit'), 48 rows: 3, 49 initialHtml: m.content, 50 initialText: m.editable, 51 defaultLang: m.language, 52 noAttach: true, 53 }) %> 54 </div> 47 55 </li> 48 56 <% }); %> … … 59 67 b.classList.toggle('is-open', open); 60 68 b.setAttribute('aria-expanded', open ? 'true' : 'false'); 61 if (open) { var ta = form.querySelector('textarea'); if (ta) ta.focus(); }69 if (open) { var ed = form.querySelector('.re-editor') || form.querySelector('textarea'); if (ed) ed.focus(); } 62 70 }); 63 71 })(); -
src/views/pages/messages.ejs
rfeced2c r5190152 78 78 <details class="msg-edit"> 79 79 <summary><%= t('fedi.edit') %></summary> 80 <form method="post" action="/fediverse/<%= n.outboxId %>/edit" class="msg-edit-form"> 81 <textarea name="text" rows="2" required><%= n.editable %></textarea> 82 <button type="submit" class="btn"><%= t('fedi.save_edit') %></button> 83 </form> 80 <div class="msg-edit-form"> 81 <%- include('../partials/reply-editor', { 82 action: '/fediverse/' + n.outboxId + '/edit', 83 placeholder: t('fedi.reply_ph'), 84 submitLabel: t('fedi.save_edit'), 85 rows: 2, 86 initialHtml: n.content, 87 initialText: n.editable, 88 defaultLang: n.language, 89 noAttach: true, 90 }) %> 91 </div> 84 92 </details> 85 93 <form method="post" action="/fediverse/<%= n.outboxId %>/delete" data-confirm="<%= t('fedi.delete_confirm') %>"> -
src/views/partials/reply-editor.ejs
rfeced2c r5190152 16 16 rows textarea rows for the no-JS fallback (default 3) 17 17 defaultLang preselected reply language (default: UI lang) 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) 18 21 %> 22 <% var _reInitHtml = (typeof initialHtml !== 'undefined' && initialHtml) || ''; %> 23 <% var _reInitText = (typeof initialText !== 'undefined' && initialText) || ''; %> 24 <% var _reNoAttach = (typeof noAttach !== 'undefined' && noAttach); %> 19 25 <% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %> 20 26 <form method="post" action="<%= action %>" class="re-form" data-re … … 36 42 <button type="button" data-cmd="list" title="<%= t('re.list') %>" aria-label="<%= t('re.list') %>">•≡</button> 37 43 <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> 44 <% if (!_reNoAttach) { %> 45 <button type="button" data-cmd="attach" title="<%= t('re.attach') %>" aria-label="<%= t('re.attach') %>">📎</button> 46 <input type="file" class="re-file" accept="image/*,audio/*,video/*" multiple hidden> 47 <% } %> 40 48 </div> 41 <input type="hidden" name="attachments" value=""> 42 <div class="re-attachments" hidden></div> 43 <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"></textarea> 49 <% if (!_reNoAttach) { %> 50 <input type="hidden" name="attachments" value=""> 51 <div class="re-attachments" hidden></div> 52 <% } %> 53 <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"><%= _reInitText %></textarea> 44 54 <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true" 45 data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden>< /div>55 data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden><%- _reInitHtml %></div> 46 56 <div class="re-foot"> 47 57 <select name="language" class="re-lang" aria-label="<%= t('re.lang') %>" hidden> -
test/rich-reply.test.js
rfeced2c r5190152 120 120 assert.equal(r, null); 121 121 }); 122 123 test('rich edit: content replaced, language updated, attachments survive', async () => { 124 const r = await AP.deliverReply(site, { 125 postId: 'p1', postSlug: 'hallo', parent, text: 'origineel', html: '', 126 attachments: [{ url: '/media/reply-media/keep.webp', mediaType: 'image/webp', name: 'blijft' }], 127 language: 'nl', 128 }); 129 const upd = await AP.deliverOutboxUpdate(site, r.id, '', { html: '<p>bewerkt met <em>nadruk</em></p>', language: 'en' }); 130 assert.ok(upd && upd.ok); 131 const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id); 132 assert.match(row.content, /bewerkt met <em>nadruk<\/em>/); 133 assert.match(row.content, /^<p><a [^>]*class="u-url mention"/); // mention re-attached inline 134 assert.equal(row.language, 'en'); 135 assert.equal(JSON.parse(row.attachments)[0].url, '/media/reply-media/keep.webp'); 136 }); 137 138 test('plain edit path unchanged; bogus language keeps the old one', async () => { 139 const r = await AP.deliverReply(site, { postId: 'p1', postSlug: 'hallo', parent, text: 'plain start', language: 'nl' }); 140 const upd = await AP.deliverOutboxUpdate(site, r.id, 'plain bewerkt', { language: '???' }); 141 assert.ok(upd && upd.ok); 142 const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(r.id); 143 assert.match(row.content, /plain bewerkt/); 144 assert.equal(row.language, 'nl'); 145 });
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)