diff --git a/assets/styles/03-components/nostr-previews.css b/assets/styles/03-components/nostr-previews.css index 22605bc..de7e604 100644 --- a/assets/styles/03-components/nostr-previews.css +++ b/assets/styles/03-components/nostr-previews.css @@ -54,3 +54,205 @@ .nostr-link:hover { background-color: rgba(108, 92, 231, 0.2); } + + +.event-container { + max-width: 800px; + margin: 2rem auto; + background: #fff; + border-radius: 8px; +} + +.event-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #eee; + padding: 1rem; +} + +.event-content { + padding: 1rem; + font-size: 1.1rem; + line-height: 1.6; +} + +.event-content img { + max-width: 100%; +} + +/* NIP-68 Picture Event Styles */ +.content-warning { + background-color: #fff3cd; + border: 2px solid #ffc107; + padding: 1rem; + border-radius: 8px; + margin-bottom: 1rem; + text-align: center; +} + +.btn-show-nsfw { + margin-top: 0.5rem; + padding: 0.5rem 1rem; + background-color: #ffc107; + border: none; + border-radius: 4px; + cursor: pointer; + font-weight: bold; +} + +.btn-show-nsfw:hover { + background-color: #e0a800; +} + +.picture-location { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + color: #555; + font-size: 0.95rem; +} + +.location-icon { + font-size: 1.2rem; +} + +.geohash { + background-color: #e9ecef; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-family: monospace; + font-size: 0.85rem; + cursor: help; +} + +.picture-source { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + font-size: 0.95rem; +} + +.source-label { + font-weight: 600; + color: #495057; + flex-shrink: 0; +} + +.source-link { + color: #0066cc; + text-decoration: none; + word-break: break-all; +} + +.source-link:hover { + text-decoration: underline; +} + +.nostr-links { + margin: 1.5rem 0; + padding: 1rem; + background-color: #f9f9f9; + border-radius: 4px; +} + +.link-list { + list-style: none; + padding-left: 0; +} + +.link-list li { + word-break: break-all; +} + +.link-type { + color: #6c757d; + font-size: 0.9rem; + margin-left: 0.5rem; +} + +.event-footer { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 1rem; + border-top: 1px solid #eee; +} + +.event-tags { + flex: 1; +} + +.event-tags ul, .event-references ul { + list-style-type: none; + padding-left: 0; +} + +.event-tags li, .event-references li { + margin-bottom: 0.5rem; +} + +.error { + color: #dc3545; + padding: 1rem; + text-align: center; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .picture-gallery { + grid-template-columns: 1fr; + } +} + +.embedded-event-card { + border: none; + border-radius: 0; + padding: 12px; + margin: 8px 0; + background: #f8f9fa; +} + +.embedded-event-card .event-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.embedded-event-card .avatar-small { + width: 24px; + height: 24px; + border-radius: 50%; +} + +.embedded-event-card .author-info { + flex: 1; +} + +.embedded-event-card .nip05 { + color: #6c757d; + font-size: 0.85em; +} + +.embedded-event-card .event-meta { + color: #6c757d; + font-size: 0.8em; +} + +.embedded-event-card .event-content { + margin: 8px 0; + line-height: 1.4; +} + +.embedded-event-card .event-footer { + text-align: right; +} + +.embedded-event-card .view-full { + color: #007bff; + text-decoration: none; + font-size: 0.85em; +} diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index 882df4c..ff33523 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -99,10 +99,10 @@ class ArticleController extends AbstractController $cacheKey = 'article_' . $article->getEventId(); $cacheItem = $articlesCache->getItem($cacheKey); - if (!$cacheItem->isHit()) { + //if (!$cacheItem->isHit()) { $cacheItem->set($converter->convertToHTML($article->getContent())); $articlesCache->save($cacheItem); - } + //} $key = new Key(); $npub = $key->convertPublicKeyToBech32($article->getPubkey()); diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index afe931e..b4fb6c8 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -48,7 +48,8 @@ class NostrClient { $this->defaultRelaySet = new RelaySet(); $this->defaultRelaySet->addRelay(new Relay('wss://theforest.nostr1.com')); // public aggregator relay - $this->defaultRelaySet->addRelay(new Relay('wss://aggr.nostr.land')); // aggregator relay, has AUTH + $this->defaultRelaySet->addRelay(new Relay('wss://nostr.land')); // aggregator relay, has AUTH + $this->defaultRelaySet->addRelay(new Relay('wss://relay.primal.net')); // profile aggregator } /** @@ -107,7 +108,7 @@ class NostrClient public function getPubkeyMetadata($pubkey): \stdClass { $relaySet = $this->defaultRelaySet; - $relaySet->addRelay(new Relay('wss://profiles.nostr1.com')); // profile aggregator + // $relaySet->addRelay(new Relay('wss://profiles.nostr1.com')); // profile aggregator $relaySet->addRelay(new Relay('wss://purplepag.es')); // profile aggregator $this->logger->info('Getting metadata for pubkey ' . $pubkey ); $request = $this->createNostrRequest( @@ -476,8 +477,11 @@ class NostrClient // Create request using the helper method $request = $this->createNostrRequest( - kinds: [KindsEnum::COMMENTS->value, KindsEnum::ZAP_RECEIPT->value], - filters: ['tag' => ['#a', [$coordinate]]], + kinds: [ + KindsEnum::COMMENTS->value, + // KindsEnum::ZAP_RECEIPT->value // Not yet + ], + filters: ['tag' => ['#A', [$coordinate]]], // #A means root event relaySet: $relaySet ); @@ -961,7 +965,7 @@ class NostrClient foreach ($relayRes as $item) { try { if (!is_object($item)) { - $this->logger->warning('Invalid response item', [ + $this->logger->warning('Invalid response item from ' . $relayUrl , [ 'relay' => $relayUrl, 'item' => $item ]); diff --git a/src/Util/CommonMark/Converter.php b/src/Util/CommonMark/Converter.php index 8866964..97c13a9 100644 --- a/src/Util/CommonMark/Converter.php +++ b/src/Util/CommonMark/Converter.php @@ -2,6 +2,7 @@ namespace App\Util\CommonMark; +use App\Enum\KindsEnum; use App\Service\NostrClient; use App\Service\RedisCacheService; use App\Util\CommonMark\ImagesExtension\RawImageLinkExtension; @@ -45,9 +46,6 @@ readonly class Converter */ public function convertToHTML(string $markdown): string { - // Preprocess nostr: links for batching - $markdown = $this->preprocessNostrLinks($markdown); - // Check if the article has more than three headings // Match all headings (from level 1 to 6) preg_match_all('/^#+\s.*$/m', $markdown, $matches); @@ -68,7 +66,7 @@ readonly class Converter ], 'embed' => [ 'adapter' => new OscaroteroEmbedAdapter(), // See the "Adapter" documentation below - 'allowed_domains' => ['youtube.com', 'x.com', 'github.com', 'fountain.fm'], + 'allowed_domains' => ['youtube.com', 'x.com', 'github.com', 'fountain.fm', 'blossom.primal.net', 'i.nostr.build'], 'fallback' => 'link' ], ]; @@ -92,16 +90,19 @@ readonly class Converter $converter = new MarkdownConverter($environment); $content = html_entity_decode($markdown); - return $converter->convert($content); + $html = $converter->convert($content); + + // Process nostr links after conversion to avoid re-processing HTML + return $this->processNostrLinks($html); } - private function preprocessNostrLinks(string $markdown): string + private function processNostrLinks(string $content): string { // Find all nostr: links - preg_match_all('/nostr:(?:npub1|nprofile1|note1|nevent1|naddr1)[^\\s<>()\\[\\]{}"\'`.,;:!?]*/', $markdown, $matches); + preg_match_all('/nostr:(?:npub1|nprofile1|note1|nevent1|naddr1)[^\\s<>()\\[\\]{}"\'`.,;:!?]*/', $content, $matches); if (empty($matches[0])) { - return $markdown; + return $content; } $links = array_unique($matches[0]); @@ -125,7 +126,7 @@ readonly class Converter case 'nprofile': /** @var NProfile $object */ $object = $decoded->data; - $pubkeys[$object->pubkey] = $bechEncoded; + $pubkeys[$object->pubkey] = $this->nostrKeyUtil->hexToNpub($object->pubkey); break; case 'note': /** @var Note $object */ @@ -147,16 +148,6 @@ readonly class Converter } } - // Fetch metadata in batch (actually, getMetadata is cached, so just prepare) - $metadata = []; - foreach (array_keys($pubkeys) as $hex) { - try { - $metadata[$hex] = $this->redisCacheService->getMetadata($hex); - } catch (\Exception $e) { - $metadata[$hex] = null; - } - } - // Fetch events in batch $events = []; if (!empty($eventIds)) { @@ -167,6 +158,26 @@ readonly class Converter } } + // Collect pubkeys from events for metadata fetching + $eventPubkeys = []; + foreach ($events as $event) { + $eventPubkeys[$event->pubkey] = true; + } + + // Fetch metadata in batch + $allHexes = array_unique(array_merge(array_keys($pubkeys), array_keys($eventPubkeys))); + $metadata = []; + try { + $fetchedMetadata = $this->redisCacheService->getMultipleMetadata($allHexes); + foreach ($allHexes as $hex) { + $metadata[$hex] = $fetchedMetadata[$hex] ?? null; + } + } catch (\Exception $e) { + foreach ($allHexes as $hex) { + $metadata[$hex] = null; + } + } + // Now, render each link foreach ($links as $link) { $bechEncoded = substr($link, 6); @@ -180,8 +191,8 @@ readonly class Converter } } - // Replace in markdown - return str_replace(array_keys($replacements), array_values($replacements), $markdown); + // Replace in content + return str_replace(array_keys($replacements), array_values($replacements), $content); } private function renderNostrLink(Bech32 $decoded, string $bechEncoded, array $metadata, array $events): string @@ -190,14 +201,12 @@ readonly class Converter case 'npub': $hex = $this->nostrKeyUtil->npubToHex($bechEncoded); $profile = $metadata[$hex] ?? null; - if ($profile && isset($profile->name)) { - return '@' . htmlspecialchars($profile->name) . ''; - } else { - return '@' . substr($bechEncoded, 5, 8) . '…'; - } + $label = $profile && isset($profile->name) ? $profile->name : $this->labelFromKey($bechEncoded); + return '@' . htmlspecialchars($label) . ''; case 'nprofile': $object = $decoded->data; - return '@' . substr($bechEncoded, 9, 8) . '…'; + $label = $this->labelFromKey($bechEncoded); + return '@' . htmlspecialchars($label) . ''; case 'note': $object = $decoded->data; $event = $events[$object->data] ?? null; @@ -208,7 +217,7 @@ readonly class Converter ]); return $pictureCardHtml; } else { - return '' . $bechEncoded . ''; + return '' . $bechEncoded . ''; } case 'nevent': $object = $decoded->data; @@ -222,13 +231,24 @@ readonly class Converter ]); return $eventCardHtml; } else { - return '' . $bechEncoded . ''; + return '' . $bechEncoded . ''; } case 'naddr': - return '' . $bechEncoded . ''; + if ($decoded->kind === KindsEnum::LONGFORM->value) { + return '' . $bechEncoded . ''; + } else { + return '' . $bechEncoded . ''; + } default: return $bechEncoded; } } + private function labelFromKey(string $npub): string + { + $start = substr($npub, 0, 5); + $end = substr($npub, -5); + return $start . '...' . $end; + } + } diff --git a/templates/components/Organisms/Comments.html.twig b/templates/components/Organisms/Comments.html.twig index 7201369..7baccbe 100644 --- a/templates/components/Organisms/Comments.html.twig +++ b/templates/components/Organisms/Comments.html.twig @@ -7,7 +7,7 @@ {% if loading %}
- {{ event|json_encode(constant('JSON_PRETTY_PRINT')) }}
-
-
-
- {% endif %}
- {{ article.topics|json_encode(constant('JSON_PRETTY_PRINT')) }}
-
-