Browse Source

refine replies and reply-to

imwald
Silberengel 5 days ago
parent
commit
dcdb3bee1e
  1. 11
      assets/controllers/article_comments_controller.js
  2. 61
      assets/controllers/comment_reply_controller.js
  3. 2
      assets/controllers/login_controller.js
  4. 15
      assets/styles/article.css
  5. 25
      src/Service/ArticleCommentThreadLoader.php
  6. 2
      templates/components/Organisms/Comments.html.twig

11
assets/controllers/article_comments_controller.js

@ -37,6 +37,12 @@ export default class extends Controller { @@ -37,6 +37,12 @@ export default class extends Controller {
void this.load();
}
buildFetchUrl() {
const u = this.urlValue;
const bust = `cb=${Date.now()}`;
return u.includes('?') ? `${u}&${bust}` : `${u}?${bust}`;
}
async load() {
const t0 = performance.now();
const perAttemptMs = 45_000;
@ -45,8 +51,11 @@ export default class extends Controller { @@ -45,8 +51,11 @@ export default class extends Controller {
const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), perAttemptMs);
try {
const res = await fetch(this.urlValue, {
// Avoid a stale 60s-cached "guest" fragment right after login (see comments fragment headers).
const res = await fetch(this.buildFetchUrl(), {
signal: controller.signal,
cache: 'no-store',
credentials: 'same-origin',
headers: { Accept: 'text/html', 'X-Requested-With': 'XMLHttpRequest' },
});
if (!res.ok) {

61
assets/controllers/comment_reply_controller.js

@ -71,7 +71,8 @@ export default class extends Controller { @@ -71,7 +71,8 @@ export default class extends Controller {
// `nostr-tools` entry pulls @noble/curves (bare spec → breaks in AssetMapper). NIP-19 only needs bech32 helpers.
const { naddrEncode, neventEncode } = await import('nostr-tools/nip19');
const link = this.buildParentBech32(naddrEncode, neventEncode);
const blurb = `> Replying to **${this.blurbLabelValue}** — [view parent](nostr:${link})\n\n`;
// NIP-22 quote line: must still mention nostr:… for server validation; UI strips this (see formatReplyBlurbForDisplay).
const blurb = `> Replying to **${this.blurbLabelValue}** (nostr:${link})\n\n`;
const unsigned = {
kind: 1111,
created_at: Math.floor(Date.now() / 1000),
@ -126,8 +127,10 @@ export default class extends Controller { @@ -126,8 +127,10 @@ export default class extends Controller {
this.toggleBtnTarget.setAttribute('aria-expanded', 'false');
}
}
if (this.refreshAfterValue && this.fragmentUrlValue) {
this.refreshThread();
if (this.refreshAfterValue) {
const publishedId =
typeof data.id === 'string' && data.id ? data.id.toLowerCase() : '';
void this.refreshThread(publishedId);
}
}
@ -156,7 +159,13 @@ export default class extends Controller { @@ -156,7 +159,13 @@ export default class extends Controller {
});
}
refreshThread() {
/**
* Reload the section HTML from the article comments fragment. After publishing, relays can lag;
* if `expectedEventIdHex` is set, re-fetch with backoff until the new note appears (or a cap is hit).
*
* @param {string} [expectedEventIdHex] lowercase 64-char hex
*/
async refreshThread(expectedEventIdHex = '') {
const wrap = this.element.closest('[data-article-comments-wrapper]');
const url =
wrap?.getAttribute('data-article-comments-url-value') ||
@ -168,16 +177,42 @@ export default class extends Controller { @@ -168,16 +177,42 @@ export default class extends Controller {
window.location.reload();
return;
}
const bust = `cb=${Date.now()}`;
const u = url.includes('?') ? `${url}&${bust}` : `${url}?${bust}`;
void fetch(u, { headers: { Accept: 'text/html', 'X-Requested-With': 'XMLHttpRequest' } })
.then((r) => (r.ok ? r.text() : Promise.reject(new Error(String(r.status)))))
.then((html) => {
const wantId =
expectedEventIdHex && /^[0-9a-f]{64}$/.test(expectedEventIdHex) ? expectedEventIdHex : '';
const maxRounds = wantId ? 14 : 1;
for (let round = 0; round < maxRounds; round += 1) {
if (round > 0) {
const delay = Math.min(1400, 200 * 2 ** (round - 1));
await new Promise((r) => setTimeout(r, delay));
}
const bust = `cb=${Date.now()}`;
const u = url.includes('?') ? `${url}&${bust}` : `${url}?${bust}`;
try {
const res = await fetch(u, {
cache: 'no-store',
credentials: 'same-origin',
headers: { Accept: 'text/html', 'X-Requested-With': 'XMLHttpRequest' },
});
if (!res.ok) {
throw new Error(String(res.status));
}
const html = await res.text();
container.innerHTML = html;
})
.catch(() => {
window.location.reload();
});
if (!wantId) {
return;
}
if (container.querySelector(`[data-event-id="${wantId}"]`)) {
return;
}
} catch {
if (round === maxRounds - 1) {
window.location.reload();
}
}
}
if (wantId) {
window.location.reload();
}
}
/**

2
assets/controllers/login_controller.js

@ -30,7 +30,7 @@ export default class extends Controller { @@ -30,7 +30,7 @@ export default class extends Controller {
return 'Authentication Successful';
})
if (!!result) {
this.component.render();
await this.component.render();
window.dispatchEvent(
new CustomEvent('unfold:auth-changed', { detail: { loggedIn: true } })
);

15
assets/styles/article.css

@ -146,13 +146,14 @@ blockquote p { @@ -146,13 +146,14 @@ blockquote p {
}
.comment__reply-blurb {
padding: 0.5rem 0.75rem 0.35rem;
margin: 0 0 0 0.25rem;
border-left: 3px solid var(--color-border, rgba(128, 128, 128, 0.45));
background: var(--color-bg-light, rgba(0, 0, 0, 0.12));
border-radius: 0 4px 4px 0;
font-size: 0.95em;
line-height: 1.45;
padding: 0.2rem 0.45rem 0.2rem 0.5rem;
margin: 0 0 0 0.2rem;
border-left: 2px solid color-mix(in srgb, var(--color-border) 50%, transparent);
background: color-mix(in srgb, var(--color-bg-light) 55%, transparent);
border-radius: 0 3px 3px 0;
font-size: 0.78em;
line-height: 1.35;
color: var(--color-text-mid);
}
.comment__reply-blurb blockquote,

25
src/Service/ArticleCommentThreadLoader.php

@ -313,12 +313,35 @@ final readonly class ArticleCommentThreadLoader @@ -313,12 +313,35 @@ final readonly class ArticleCommentThreadLoader
}
}
}
$ev->unfold_reply_blurb = $blurb;
$ev->unfold_reply_blurb = $this->formatReplyBlurbForDisplay($blurb);
$ev->unfold_body = $split['body'];
$ev->unfold_depth = $id === '' || !ctype_xdigit($id) ? 0 : $this->threadDepthCapped($id, $parentOf, 3);
}
}
/**
* NIP-22 storage often includes a markdown link to the parent; hide that in the UI and show plain “replying to …” text.
*/
private function formatReplyBlurbForDisplay(?string $blurb): ?string
{
if ($blurb === null) {
return null;
}
$s = trim($blurb);
if ($s === '') {
return null;
}
$s = preg_replace('/\s*\[[^\]]+\]\(nostr:[^)]+\)/u', '', $s) ?? $s;
$s = preg_replace('/\s*\(nostr:[^)]+\)/u', '', $s) ?? $s;
$s = rtrim($s, " \t");
$s = preg_replace('/\s*—\s*$/u', '', $s) ?? $s;
$s = rtrim($s, " \t");
$s = preg_replace('/\*\*([^*]+)\*\*/u', '$1', $s) ?? $s;
$s = trim($s);
return $s === '' ? null : $s;
}
/**
* @return array{blurb: string|null, body: string}
*/

2
templates/components/Organisms/Comments.html.twig

@ -65,7 +65,7 @@ @@ -65,7 +65,7 @@
{% set cts = item.created_at|default(null) %}
{% set cdepth = item.unfold_depth|default(0) %}
{% set is_nip18_repost = item.kind is defined and (item.kind == 6 or item.kind == 16) %}
<div class="card comment comment--depth-{{ cdepth }}">
<div class="card comment comment--depth-{{ cdepth }}"{% if cid != '' %} data-event-id="{{ cid|e('html_attr') }}"{% endif %}>
<div class="metadata">
<p>
{% if item.kind is defined and item.kind == 1 %}

Loading…
Cancel
Save