From 469ff20431ff498891faee22179fa46cd550c09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nu=C5=A1a=20Puk=C5=A1i=C4=8D?= Date: Sat, 24 May 2025 19:12:53 +0200 Subject: [PATCH] Comments for articles --- assets/styles/card.css | 17 +++ assets/styles/notice.css | 1 - src/Controller/ArticleController.php | 2 +- src/Controller/DefaultController.php | 89 +++++++-------- src/Enum/KindsEnum.php | 1 + src/Service/NostrClient.php | 101 +++++++++++++----- .../components/Organisms/Comments.html.twig | 10 +- templates/pages/article.html.twig | 8 +- 8 files changed, 153 insertions(+), 76 deletions(-) diff --git a/assets/styles/card.css b/assets/styles/card.css index a7510ab..f328ad1 100644 --- a/assets/styles/card.css +++ b/assets/styles/card.css @@ -47,3 +47,20 @@ h2.card-title { object-fit: cover; } +.card.comment { + display: flex; + flex-direction: column; + background-color: var(--color-bg-light); + padding: 10px; +} + +.card.comment .metadata { + display: flex; + align-items: center; + justify-content: space-between; +} + +.card.comment .metadata p { + margin: 0; + padding: 0; +} diff --git a/assets/styles/notice.css b/assets/styles/notice.css index d8d908c..a2b1558 100644 --- a/assets/styles/notice.css +++ b/assets/styles/notice.css @@ -10,6 +10,5 @@ .notice.info { background-color: rgba(95, 115, 85, 0.15); /* Light version of --color-primary */ - border-left: 4px solid var(--color-primary); /* Solid border using primary color */ color: var(--color-text); /* Use theme text color for better contrast */ } diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php index 5e69a18..58f5a61 100644 --- a/src/Controller/ArticleController.php +++ b/src/Controller/ArticleController.php @@ -125,7 +125,7 @@ class ArticleController extends AbstractController $author = $redisCacheService->getMetadata($npub); - return $this->render('Pages/article.html.twig', [ + return $this->render('pages/article.html.twig', [ 'article' => $article, 'author' => $author, 'npub' => $npub, diff --git a/src/Controller/DefaultController.php b/src/Controller/DefaultController.php index 4fbe3b5..637df1e 100644 --- a/src/Controller/DefaultController.php +++ b/src/Controller/DefaultController.php @@ -60,10 +60,10 @@ class DefaultController extends AbstractController }); $list = []; - $slugs = []; $coordinates = []; // Store full coordinates (kind:author:slug) $category = []; + // Extract category metadata and article coordinates foreach ($catIndex->getTags() as $tag) { if ($tag[0] === 'title') { $category['title'] = $tag[1]; @@ -72,74 +72,77 @@ class DefaultController extends AbstractController $category['summary'] = $tag[1]; } if ($tag[0] === 'a') { - $parts = explode(':', $tag[1]); - if (count($parts) === 3) { - $slugs[] = $parts[2]; - $coordinates[] = $tag[1]; // Store the full coordinate - } + $coordinates[] = $tag[1]; // Store the full coordinate } } - if (!empty($slugs)) { + // Limit to first 9 coordinates to avoid excessive processing + $coordinates = array_slice($coordinates, 0, 9); + + if (!empty($coordinates)) { + // Extract slugs for elasticsearch query + $slugs = array_map(function($coordinate) { + $parts = explode(':', $coordinate); + return count($parts) === 3 ? $parts[2] : ''; + }, $coordinates); + $slugs = array_filter($slugs); // Remove empty values + + // Try to fetch articles from elasticsearch first $query = new Terms('slug', array_values($slugs)); $articles = $finder->find($query); // Create a map of slug => item to remove duplicates $slugMap = []; - foreach ($articles as $item) { $slug = $item->getSlug(); - - if ($slug !== '' && !isset($slugMap[$slug])) { + if ($slug !== '') { $slugMap[$slug] = $item; } } - // Find missing articles based on coordinates + // Find missing coordinates $missingCoordinates = []; - $missingIndexes = []; - - for ($i = 0; $i < count($slugs); $i++) { - $slug = $slugs[$i]; - if (!isset($slugMap[$slug])) { - $missingCoordinates[] = $coordinates[$i]; - $missingIndexes[$coordinates[$i]] = $i; // Track original position + foreach ($coordinates as $coordinate) { + $parts = explode(':', $coordinate); + if (count($parts) === 3 && !isset($slugMap[$parts[2]])) { + $missingCoordinates[] = $coordinate; } } - // If we have missing articles, fetch them from nostr + // If we have missing articles, fetch them directly using NostrClient's getArticlesByCoordinates if (!empty($missingCoordinates)) { - $logger->info('Fetching missing articles', [ + + $logger->info('There were missing articles', [ 'missing' => $missingCoordinates ]); - try { - $nostrArticles = $nostrClient->getArticlesByCoordinates($missingCoordinates); - - foreach ($nostrArticles as $coordinate => $event) { - $parts = explode(':', $coordinate); - if (count($parts) === 3) { - $article = $articleFactory->createFromLongFormContentEvent($event); - - // Add to the slugMap - $slugMap[$article->getSlug()] = $article; - } - } - } catch (\Exception $e) { - $logger->error('Error fetching missing articles', [ - 'error' => $e->getMessage() - ]); - } +// try { +// $nostrArticles = $nostrClient->getArticlesByCoordinates($missingCoordinates); +// +// foreach ($nostrArticles as $coordinate => $event) { +// $parts = explode(':', $coordinate); +// if (count($parts) === 3) { +// $article = $articleFactory->createFromLongFormContentEvent($event); +// // Save article to database for future queries +// $nostrClient->saveEachArticleToTheDatabase($article); +// // Add to the slugMap +// $slugMap[$article->getSlug()] = $article; +// } +// } +// } catch (\Exception $e) { +// $logger->error('Error fetching missing articles', [ +// 'error' => $e->getMessage() +// ]); +// } } - // Reorder by the original $slugs to maintain order - $results = []; - foreach ($slugs as $slug) { - if (isset($slugMap[$slug])) { - $results[] = $slugMap[$slug]; + // Build ordered list based on original coordinates order + foreach ($coordinates as $coordinate) { + $parts = explode(':', $coordinate); + if (count($parts) === 3 && isset($slugMap[$parts[2]])) { + $list[] = $slugMap[$parts[2]]; } } - $list = array_values($results); } return $this->render('pages/category.html.twig', [ diff --git a/src/Enum/KindsEnum.php b/src/Enum/KindsEnum.php index f0a4e60..714f1fd 100644 --- a/src/Enum/KindsEnum.php +++ b/src/Enum/KindsEnum.php @@ -16,6 +16,7 @@ enum KindsEnum: int case LONGFORM = 30023; // NIP-23 case LONGFORM_DRAFT = 30024; // NIP-23 case PUBLICATION_INDEX = 30040; + case ZAP = 9735; // NIP-57, Zaps case HIGHLIGHTS = 9802; case RELAY_LIST = 10002; // NIP-65, Relay list metadata case APP_DATA = 30078; // NIP-78, Arbitrary custom app data diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index de17acc..996e1e6 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -336,42 +336,89 @@ class NostrClient } /** + * Get comments for a specific coordinate + * + * @param string $coordinate The event coordinate (kind:pubkey:identifier) + * @return array Array of comment events * @throws \Exception */ - public function getComments($coordinate): array + public function getComments(string $coordinate): array { - $list = []; - $parts = explode(':', $coordinate); + $this->logger->info('Getting comments for coordinate', ['coordinate' => $coordinate]); - $subscription = new Subscription(); - $subscriptionId = $subscription->setId(); - $filter = new Filter(); - $filter->setKinds([KindsEnum::COMMENTS, KindsEnum::TEXT_NOTE]); - $filter->setTag('#a', [$coordinate]); - $requestMessage = new RequestMessage($subscriptionId, [$filter]); + // Get author from coordinate, then relays + $parts = explode(':', $coordinate); + if (count($parts) !== 3) { + throw new \InvalidArgumentException('Invalid coordinate format, expected kind:pubkey:identifier'); + } + $kind = (int)$parts[0]; + $pubkey = $parts[1]; + $identifier = $parts[2]; + // Get relays for the author + $authorRelays = $this->getTopReputableRelaysForAuthor($pubkey); + // Turn into a relaySet + $relaySet = $this->createRelaySet($authorRelays); - $request = new Request($this->defaultRelaySet, $requestMessage); + // Create request using the helper method + $request = $this->createNostrRequest( + kinds: [KindsEnum::COMMENTS->value, KindsEnum::TEXT_NOTE->value], + filters: ['tag' => ['#a', [$coordinate], '#p', [$pubkey]]], + relaySet: $relaySet + ); - $response = $request->send(); - // response is an array of arrays - foreach ($response as $value) { - foreach ($value as $item) { - switch ($item->type) { - case 'EVENT': - dump($item); - $list[] = $item; - break; - case 'AUTH': - // throw new UnauthorizedHttpException('', 'Relay requires authentication'); - case 'ERROR': - case 'NOTICE': - // throw new \Exception('An error occurred'); - default: - // nothing to do here + // Process the response and deduplicate by eventId + $uniqueEvents = []; + $this->processResponse($request->send(), function($event) use (&$uniqueEvents, $pubkey) { + $this->logger->debug('Received comment event', ['event_id' => $event->id]); + // If event has p tag with the pubkey, it's a comment + // Loop tags, look for 'p' tag + foreach ($event->tags as $tag) { + if ($tag[0] === 'p' && $tag[1] === $pubkey) { + $uniqueEvents[$event->id] = $event; + break; } } + return null; // We'll handle the collection ourselves + }); + + return array_values($uniqueEvents); + } + + /** + * Get zap events for a specific event + * + * @param string $coordinate The event coordinate (kind:pubkey:identifier) + * @return array Array of zap events + * @throws \Exception + */ + public function getZapsForEvent(string $coordinate): array + { + $this->logger->info('Getting zaps for coordinate', ['coordinate' => $coordinate]); + + // Parse the coordinate to get pubkey + $parts = explode(':', $coordinate); + if (count($parts) !== 3) { + throw new \InvalidArgumentException('Invalid coordinate format, expected kind:pubkey:identifier'); } - return $list; + $pubkey = $parts[1]; + + // Get author's relays for better chances of finding zaps + $authorRelays = $this->getTopReputableRelaysForAuthor($pubkey); + $relaySet = $this->createRelaySet($authorRelays); + + // Create request using the helper method + // Zaps are kind 9735 + $request = $this->createNostrRequest( + kinds: [KindsEnum::ZAP], + filters: ['tag' => ['#a', [$coordinate]]], + relaySet: $relaySet + ); + + // Process the response + return $this->processResponse($request->send(), function($event) { + $this->logger->debug('Received zap event', ['event_id' => $event->id]); + return $event; + }); } /** diff --git a/templates/components/Organisms/Comments.html.twig b/templates/components/Organisms/Comments.html.twig index 0863c44..b342df3 100644 --- a/templates/components/Organisms/Comments.html.twig +++ b/templates/components/Organisms/Comments.html.twig @@ -1,7 +1,13 @@
{% for item in list %} -
- {{ item.content }} +
+ +
+

{{ item.content }}

+
{% endfor %}
diff --git a/templates/pages/article.html.twig b/templates/pages/article.html.twig index dbc82ed..1969fc9 100644 --- a/templates/pages/article.html.twig +++ b/templates/pages/article.html.twig @@ -58,19 +58,23 @@ {{ content|raw }}
+ {% if article.topics|length > 0 %}
{% for tag in article.topics %} {{ tag }} {% endfor %}
+ {% endif %} -{#
#}
+    
+ + {#
#}
 {#        {{ article.content }}#}
 {#    
#} - + {% endblock %} {% block aside %}