Browse Source

bug-fixes

imwald
Silberengel 3 days ago
parent
commit
4f1173ae67
  1. 59
      assets/styles/article.css
  2. 28
      assets/styles/components/_nostr_previews.scss
  3. 32
      assets/styles/nostr-previews.css
  4. 25
      config/unfold.yaml
  5. 103
      src/Command/PrewarmCommand.php
  6. 18
      src/Service/MagazineContentService.php
  7. 67
      src/Service/NostrClient.php
  8. 37
      templates/components/Molecules/NostrPreviewContent.html.twig

59
assets/styles/article.css

@ -86,13 +86,14 @@ @@ -86,13 +86,14 @@
gap: 0.35em;
}
blockquote {
/* Article body only — avoid 50px vertical margins on comment / quote cards (Atoms:Content markdown). */
.article-main blockquote {
border-left: 6px solid var(--color-bg-light);
padding-left: 3px;
margin: 50px 0 50px 3px;
}
blockquote p {
.article-main blockquote p {
font-size: 1.6rem;
font-style: italic;
color: var(--color-text-mid);
@ -180,20 +181,22 @@ blockquote p { @@ -180,20 +181,22 @@ blockquote p {
margin-top: 0.75rem;
}
/* Thread: no depth indent; one accent color for all replies; compact vertical rhythm */
/* Thread: no depth indent; one accent color for all replies */
.comments {
display: flex;
flex-direction: column;
gap: 0.4rem;
gap: 0.55rem;
}
.comments .card.comment {
.comments .card.comment,
.comments-quotes__list .card.comment {
margin-left: 0;
margin-bottom: 0;
padding: 0.5rem 0.65rem 0.5rem 0.7rem;
padding: 0.75rem 0.9rem 0.85rem;
border-radius: 6px;
border: 1px solid var(--color-border);
border-left: 3px solid var(--color-primary);
gap: 0.5rem;
}
.comments .card.comment--depth-0,
@ -204,19 +207,53 @@ blockquote p { @@ -204,19 +207,53 @@ blockquote p {
border-left-color: var(--color-primary);
}
.comments .card.comment .metadata {
margin-bottom: 0.4rem;
.comments .card.comment .metadata,
.comments-quotes__list .card.comment .metadata {
margin-bottom: 0;
}
.comment__reply-blurb {
padding: 0.2rem 0.45rem 0.2rem 0.5rem;
padding: 0.4rem 0.55rem 0.45rem 0.55rem;
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;
font-size: 0.82em;
line-height: 1.45;
color: var(--color-text-mid);
}
/* Markdown blockquotes inside note bodies: tight rhythm (not .article-main blockquote). */
.comments .card.comment .card-body blockquote,
.comments-quotes__list .card.comment .card-body blockquote {
margin: 0.35rem 0 0.5rem;
padding: 0.3rem 0 0.35rem 0.65rem;
border-left: 3px solid color-mix(in srgb, var(--color-primary) 40%, var(--color-border));
}
.comments .card.comment .card-body blockquote p,
.comments-quotes__list .card.comment .card-body blockquote p {
font-size: 1em;
line-height: 1.45;
font-style: italic;
color: var(--color-text-mid);
margin: 0;
padding-left: 0;
}
.comments .card.comment .card-body > :first-child,
.comments-quotes__list .card.comment .card-body > :first-child {
margin-top: 0;
}
.comments .card.comment .card-body > :last-child,
.comments-quotes__list .card.comment .card-body > :last-child {
margin-bottom: 0;
}
.comments .card.comment .card-body,
.comments-quotes__list .card.comment .card-body {
line-height: 1.52;
}
.comment__reply-blurb blockquote,

28
assets/styles/components/_nostr_previews.scss

@ -67,6 +67,34 @@ @@ -67,6 +67,34 @@
flex-direction: column;
gap: 0.35rem;
}
.nostr-address-preview {
position: relative;
}
.nostr-address-preview--with-menu .nostr-address-preview__body {
padding-right: 2.25rem;
}
.nostr-address-preview__body {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 0.45rem 0.75rem 0.55rem;
.card-title,
.card-text {
margin: 0;
}
}
.nostr-address-preview .nostr-preview-card__menu {
position: absolute;
top: 0.35rem;
right: 0.4rem;
margin: 0;
z-index: 1;
}
}
.nostr-previews {

32
assets/styles/nostr-previews.css

@ -85,12 +85,44 @@ @@ -85,12 +85,44 @@
gap: 0.35rem;
}
/* naddr previews: avoid global h1–h6 margins + relay tag order quirks; menu out of flow */
.nostr-preview .nostr-address-preview {
position: relative;
}
.nostr-preview .nostr-address-preview--with-menu .nostr-address-preview__body {
padding-right: 2.25rem;
}
.nostr-preview .nostr-address-preview__body {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 0.45rem 0.75rem 0.55rem;
}
.nostr-preview .nostr-address-preview__body .card-title {
margin: 0;
}
.nostr-preview .nostr-address-preview__body .card-text {
margin: 0;
}
.nostr-preview-card__menu {
display: flex;
justify-content: flex-end;
margin-bottom: 0.35rem;
}
.nostr-preview .nostr-address-preview .nostr-preview-card__menu {
position: absolute;
top: 0.35rem;
right: 0.4rem;
margin: 0;
z-index: 1;
}
.nostr-previews h6 {
font-size: 0.9rem;
margin-bottom: 1rem;

25
config/unfold.yaml

@ -8,17 +8,26 @@ parameters: @@ -8,17 +8,26 @@ parameters:
og_headline: 'Nostr, Curated Thoughtfully'
og_subheading: 'Imwald Blog by Laeserin'
default_relay: 'wss://TheForest.nostr1.com'
default_relay: 'wss://theforest.nostr1.com'
# Extra wss:// URLs for article sync (articles:get), comment threads (NIP-22 / getArticleDiscussion),
# and any request that merges the default set with author-specific relays. default_relay is first; duplicates ignored.
article_relays: ['wss://christpill.nostr1.com', 'wss://nostr.land', 'wss://nostr.wine', 'wss://nostr21.com', 'wss://nostr.sovbit.host']
article_relays: [
'wss://christpill.nostr1.com',
'wss://nostr.land',
'wss://nostr.wine',
'wss://nostr21.com',
'wss://nostr.sovbit.host',
'wss://orly-relay.imwald.eu'
]
# Kind-0 / profile fetches (author metadata, prewarm). Tried first, then default + article_relays (deduped).
profile_relays:
- 'wss://relay.damus.io'
- 'wss://nos.lol'
- 'wss://profiles.nostr1.com'
- 'wss://thecitadel.nostr1.com'
- 'wss://nostr.wine'
profile_relays: [
'wss://relay.damus.io',
'wss://nos.lol',
'wss://profiles.nostr1.com',
'wss://thecitadel.nostr1.com',
'wss://nostr.wine',
'wss://orly-relay.imwald.eu'
]
# Example:
# article_relays:
# - 'wss://nos.lol'

103
src/Command/PrewarmCommand.php

@ -360,10 +360,13 @@ final class PrewarmCommand extends Command @@ -360,10 +360,13 @@ final class PrewarmCommand extends Command
$io->success(sprintf('Warmed metadata for %d of %d author(s).', $n, $total));
if ($toWarm !== []) {
$domain = trim((string) $this->params->get('nip05_domain'));
if ($domain !== '') {
$this->waitForSiteWellKnownBeforeVerification($io, $domain);
}
$io->writeln('Verifying <comment>NIP-05</comment> (HTTPS <comment>/.well-known/nostr.json</comment>, per identifier)…');
$nt = 0;
$nv = 0;
$domain = trim((string) $this->params->get('nip05_domain'));
foreach ($toWarm as $hex) {
if (64 !== \strlen($hex) || !ctype_xdigit($hex)) {
continue;
@ -471,6 +474,104 @@ final class PrewarmCommand extends Command @@ -471,6 +474,104 @@ final class PrewarmCommand extends Command
return Command::SUCCESS;
}
private function waitForSiteWellKnownBeforeVerification(SymfonyStyle $io, string $domain): void
{
$expected = [];
foreach ($this->featuredAuthorRepository->findAllListedOrderByLocalPart() as $row) {
$local = trim((string) $row->getLocalPart());
$hex = strtolower(trim((string) $row->getPubkeyHex()));
if ($local === '' || 64 !== \strlen($hex) || !ctype_xdigit($hex)) {
continue;
}
$expected[$local] = $hex;
}
if ($expected === []) {
return;
}
$io->writeln(sprintf(
'Ensuring site NIP-05 directory is current before verification (<comment>%s</comment>, names: <info>%d</info>)…',
$domain,
\count($expected)
));
$url = 'https://'.$domain.'/.well-known/nostr.json';
$attempt = 0;
$maxAttempts = 4;
while ($attempt < $maxAttempts) {
$attempt++;
$payload = $this->fetchWellKnownNamesMap($url);
if ($payload !== null && $this->wellKnownHasExpectedNames($payload, $expected)) {
$io->writeln(sprintf(' <info>OK</info> /.well-known/nostr.json is up-to-date (attempt %d/%d).', $attempt, $maxAttempts));
return;
}
if ($attempt < $maxAttempts) {
usleep(1_500_000);
}
}
$io->warning('Site /.well-known/nostr.json did not reflect current featured authors before verification; NIP-05 checks may fail transiently.');
}
/**
* @return array<string, string>|null
*/
private function fetchWellKnownNamesMap(string $url): ?array
{
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'header' => "User-Agent: Unfold-Prewarm/1.0\r\nAccept: application/json\r\n",
'timeout' => 8,
'ignore_errors' => true,
],
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
],
]);
$raw = @file_get_contents($url, false, $ctx);
if ($raw === false) {
return null;
}
try {
$decoded = json_decode($raw, true, 512, \JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return null;
}
if (!\is_array($decoded) || !isset($decoded['names']) || !\is_array($decoded['names'])) {
return null;
}
$out = [];
foreach ($decoded['names'] as $k => $v) {
if (!\is_string($k) || !\is_string($v)) {
continue;
}
$key = trim($k);
$hex = strtolower(trim($v));
if ($key === '' || 64 !== \strlen($hex) || !ctype_xdigit($hex)) {
continue;
}
$out[$key] = $hex;
}
return $out;
}
/**
* @param array<string, string> $names
* @param array<string, string> $expected
*/
private function wellKnownHasExpectedNames(array $names, array $expected): bool
{
foreach ($expected as $local => $hex) {
if (!isset($names[$local]) || !hash_equals($hex, $names[$local])) {
return false;
}
}
return true;
}
/**
* Absolute used/budget wall seconds for the comment phase, e.g. "127.4/600 s" (not a percentage).
*/

18
src/Service/MagazineContentService.php

@ -290,6 +290,9 @@ final class MagazineContentService @@ -290,6 +290,9 @@ final class MagazineContentService
{
$n = 0;
foreach ($this->getCategorySlugsFromStore() as $catSlug) {
// If a category 30040 wasn't persisted during the refresh phase (relay errors/timeouts),
// try one direct fetch here so long-form ingest and reports are not silently incomplete.
$this->warmCategoryIndexIfMissing($catSlug);
$all = $this->findAllLongformCoordinatesForCategory($catSlug);
if ($all === []) {
continue;
@ -331,8 +334,23 @@ final class MagazineContentService @@ -331,8 +334,23 @@ final class MagazineContentService
$totResolved = 0;
$totMissing = 0;
foreach ($this->getCategorySlugsFromStore() as $slug) {
$this->warmCategoryIndexIfMissing($slug);
$catIndex = $this->store->getCategory($slug);
if ($catIndex === null) {
$totMissing++;
$categories[] = [
'slug' => $slug,
'title' => $slug,
'event_id' => '',
'listed_total' => 0,
'resolved_total' => 0,
'missing_total' => 1,
'entries' => [[
'coordinate' => 'category:'.$slug,
'status' => 'missing',
'reason' => 'category_index_unavailable',
]],
];
continue;
}
$title = $slug;

67
src/Service/NostrClient.php

@ -2847,6 +2847,43 @@ class NostrClient @@ -2847,6 +2847,43 @@ class NostrClient
'group_key' => $gkey,
'authors_filter' => $g['pubkey'],
]);
// Some relays fail to satisfy combined authors+#d filters for parameterized replaceables.
// Fallback: query by #d only, then enforce author and d-tag match client-side.
$fallbackReq = $this->createNostrRequest(
[$kindEnum],
['tag' => ['#d', $dTags]],
$this->defaultRelaySet,
);
$fallbackEvents = $this->processResponse(
$fallbackReq->send(),
static fn (object $event) => $event,
);
$fallbackMatched = [];
$expectedPubkey = strtolower((string) $g['pubkey']);
$expectedD = array_fill_keys($dTags, true);
foreach ($fallbackEvents as $ev) {
if (!\is_object($ev)) {
continue;
}
$evPubkey = strtolower((string) ($ev->pubkey ?? ''));
if ($evPubkey !== $expectedPubkey) {
continue;
}
$evD = self::eventDTagValue($ev);
if ($evD === null || !isset($expectedD[$evD])) {
continue;
}
$fallbackMatched[] = $ev;
}
$this->logger->info('[longform_ingest] ingestLongform: fallback #d-only query result', [
'group_key' => $gkey,
'fallback_raw_wire_count' => \count($fallbackEvents),
'fallback_matched_count' => \count($fallbackMatched),
]);
if ($fallbackMatched !== []) {
$events = $fallbackMatched;
$rawCount = \count($events);
}
}
$merged = self::mergeNip33ParameterizedWireEvents($events);
$mergedDetail = [];
@ -2860,13 +2897,43 @@ class NostrClient @@ -2860,13 +2897,43 @@ class NostrClient
'merged_count' => \count($merged),
'one_row_per_nip33_address' => $mergedDetail,
]);
$kindInt = (int) $g['kind'];
$authorHex = strtolower((string) $g['pubkey']);
$expectedAddresses = [];
foreach ($dTags as $dt) {
$expectedAddresses[$kindInt.':'.$authorHex.':'.$dt] = true;
}
$seenAddresses = [];
foreach ($merged as $event) {
if (!\is_object($event)) {
continue;
}
$addr = self::nip33ParameterizedReplaceableAddress($event);
if ($addr !== null) {
$seenAddresses[$addr] = true;
}
$article = $this->articleFactory->createFromLongFormContentEvent($event);
$this->saveEachArticleToTheDatabase($article);
}
foreach (array_keys($expectedAddresses) as $coordinate) {
if (isset($seenAddresses[$coordinate])) {
continue;
}
$this->logger->notice('[longform_ingest] ingestLongform: address missing after batch merge; trying author NIP-65 relays', [
'coordinate' => $coordinate,
]);
$byCoord = $this->getArticlesByCoordinates([$coordinate]);
$evExtra = $byCoord[$coordinate] ?? null;
if ($evExtra === null) {
$this->logger->warning('[longform_ingest] ingestLongform: still no event for coordinate (not on default or author relays)', [
'coordinate' => $coordinate,
]);
continue;
}
$article = $this->articleFactory->createFromLongFormContentEvent($evExtra);
$this->saveEachArticleToTheDatabase($article);
}
} catch (\Throwable $e) {
$this->logger->error(
sprintf('[longform_ingest] ingestLongform: exception in group %s: %s', (string) $gkey, $e->getMessage()),

37
templates/components/Molecules/NostrPreviewContent.html.twig

@ -1,21 +1,36 @@ @@ -1,21 +1,36 @@
{% if preview.type == 'naddr' %}
<div class="card nostr-address-preview">
{% set _na_share = nostr_event_share(preview) %}
{% set naddr_title = null %}
{% set naddr_summary = null %}
{% if preview.tags is defined %}
{% for tag in preview.tags %}
{% if tag[0] == 'title' and naddr_title is null and tag[1] is defined and tag[1]|default('')|trim != '' %}
{% set naddr_title = tag[1] %}
{% elseif tag[0] == 'summary' and naddr_summary is null and tag[1] is defined and tag[1]|default('')|trim != '' %}
{% set naddr_summary = tag[1] %}
{% endif %}
{% endfor %}
{% endif %}
{% set _na_share = nostr_event_share(preview) %}
<div class="card nostr-address-preview{% if _na_share %} nostr-address-preview--with-menu{% endif %}">
{% if _na_share %}
<div class="nostr-preview-card__menu">
{% include 'components/Molecules/NostrShareMenu.html.twig' with { share: _na_share, event_menu: true } only %}
</div>
{% endif %}
{% for tag in preview.tags %}
{% if tag[0] == 'title' %}
<div class="card-header">
<h5 class="card-title">{{ tag[1] }}</h5>
</div>
<div class="card-body nostr-address-preview__body">
{% if naddr_title %}
<h5 class="card-title">{{ naddr_title }}</h5>
{% endif %}
{% if tag[0] == 'summary' %}
<p class="card-text">{{ tag[1] }}</p>
{% endif %}
{% endfor %}
<p class="card-text">
{% if naddr_summary %}
{{ naddr_summary }}
{% elseif preview.content is defined and preview.content|trim != '' %}
{{ preview.content|length > 220 ? preview.content|slice(0, 220) ~ '…' : preview.content }}
{% else %}
<span class="text-subtle">—</span>
{% endif %}
</p>
</div>
</div>
{% elseif preview.type == 'nevent' %}
{% if preview.kind == 9802 %}

Loading…
Cancel
Save