Changeset e00c0e9 in Klonkt


Ignore:
Timestamp:
07/01/2026 10:06:57 PM (2 months ago)
Author:
roboburr <roboburr@…>
Branches:
main
Children:
254939c
Parents:
d11e308
Message:

fix(fediverse): auto-link bare URLs in outgoing posts and replies

A plain http(s) URL typed in a post or reply federated as plain text; it now
becomes a clickable link on the federated copy. Existing links are never wrapped
twice, attribute values never match, and trailing sentence punctuation stays
outside the link (Mastodon-style).

  • src/services/ActivityPubService.js — linkUrls() helper (anchor-aware split + bounded URL regex); applied in buildNote (posts) and both reply content builders (send + edit).
  • test/url-linkify.test.js — bare URL, fragment vs hashtag, no double-wrap, attributes untouched.
  • CHANGELOG(.nl/.de).md — under Fixed.

Closes prutfolio-src-412.

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

Files:
1 added
4 edited

Legend:

Unmodified
Added
Removed
  • CHANGELOG.de.md

    rd11e308 re00c0e9  
    3535  ist die Datei verbreitet — erneutes "Schließen" wäre Scheinsicherheit. Der Editor sperrt die Wahl
    3636  nach dem Öffnen und warnt dich, bevor du sie ankreuzt.
     37
     38### Behoben
     39- **Nackte Webadressen werden im Fediverse zu Links.** Eine einfache URL in einem Beitrag oder einer
     40  Antwort föderiert jetzt als klickbarer Link statt als reiner Text.
    3741
    3842## [1.2.0] — 2026-07-01
  • CHANGELOG.md

    rd11e308 re00c0e9  
    3030  has spread, so "closing" it again would be false security — the editor now locks the choice after
    3131  opening and warns you before you tick it.
     32
     33### Fixed
     34- **Plain web addresses become links on the fediverse.** A bare URL typed in a post or reply now
     35  federates as a clickable link instead of plain text.
    3236
    3337## [1.2.0] — 2026-07-01
  • CHANGELOG.nl.md

    rd11e308 re00c0e9  
    3333  is het bestand verspreid — weer "sluiten" zou schijnveiligheid zijn. De editor vergrendelt de keuze
    3434  na het openen en waarschuwt je voordat je 'm aanvinkt.
     35
     36### Opgelost
     37- **Kale webadressen worden links op de fediverse.** Een losse URL in een post of reactie federeert
     38  nu als klikbare link in plaats van platte tekst.
    3539
    3640## [1.2.0] — 2026-07-01
  • src/services/ActivityPubService.js

    rd11e308 re00c0e9  
    343343  body = body.replace(/\r?\n/g, '<br>');
    344344  body = linkHashtags(base, body); // link inline #hashtags in the post body too
     345  body = linkUrls(body);           // bare URLs → clickable links on the federated copy
    345346  // Append the tags-field hashtags to the content so Mastodon renders them as clickable
    346347  // hashtags (a Hashtag that's only in the `tag` array isn't shown inline). CamelCase
     
    13711372    `${pre}<a href="${base}/tag/${encodeURIComponent(tag.toLowerCase())}" class="mention hashtag" rel="tag">#${tag}</a>`);
    13721373}
     1374// Auto-link bare http(s) URLs in already-safe HTML (federated copies). Splits on existing
     1375// <a>…</a> so a linked URL is never wrapped twice; requires start/whitespace/'>' before the
     1376// URL so attribute values (src="https://…") never match. Trailing sentence punctuation stays
     1377// outside the link (Mastodon-style).
     1378function linkUrls(html) {
     1379  const parts = String(html || '').split(/(<a\b[^>]*>[\s\S]*?<\/a>)/gi);
     1380  for (let i = 0; i < parts.length; i++) {
     1381    if (/^<a\b/i.test(parts[i])) continue; // already a link → leave as-is
     1382    parts[i] = parts[i].replace(/(^|[\s>])(https?:\/\/[^\s<]+?)([.,;:!?)\]»]*)(?=$|[\s<])/g,
     1383      (m, pre, url, trail) => `${pre}<a href="${url.replace(/"/g, '%22')}" rel="nofollow noopener" target="_blank">${url}</a>${trail}`);
     1384  }
     1385  return parts.join('');
     1386}
    13731387// Extract the AP Hashtag tag objects from already-linked reply content.
    13741388function hashtagTags(base, content) {
     
    14971511  const mention = parent.actor_uri
    14981512    ? `<a href="${escHtml(parent.actor_url || parent.actor_uri)}" class="u-url mention" data-actor="${escHtml(parent.actor_uri)}">${escHtml(dispHandle)}</a> ` : '';
    1499   const content = `<p>${mention}${linkHashtags(base, mres.html)}</p>`;
     1513  const content = `<p>${mention}${linkUrls(linkHashtags(base, mres.html))}</p>`;
    15001514  // Dedup: skip if the exact same reply was already sent (double-submit guard).
    15011515  const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? LIMIT 1')
     
    16571671    ? `<a href="${escHtml(toProfile)}" class="u-url mention" data-actor="${escHtml(row.to_actor)}">${escHtml(toHandle)}</a> ` : '';
    16581672  const mres = await resolveMentionsInText(base, escHtml(text).replace(/\r?\n/g, '<br>'));
    1659   const content = `<p>${mention}${linkHashtags(base, mres.html)}</p>`;
     1673  const content = `<p>${mention}${linkUrls(linkHashtags(base, mres.html))}</p>`;
    16601674  db.prepare('UPDATE ap_outbox SET content = ? WHERE id = ?').run(content, outboxId);
    16611675  const note = buildReplyNote(base, site, iStmts().getO.get(outboxId));
Note: See TracChangeset for help on using the changeset viewer.