Browse Source

Comments for articles

imwald
Nuša Pukšič 8 months ago
parent
commit
469ff20431
  1. 17
      assets/styles/card.css
  2. 1
      assets/styles/notice.css
  3. 2
      src/Controller/ArticleController.php
  4. 89
      src/Controller/DefaultController.php
  5. 1
      src/Enum/KindsEnum.php
  6. 101
      src/Service/NostrClient.php
  7. 10
      templates/components/Organisms/Comments.html.twig
  8. 8
      templates/pages/article.html.twig

17
assets/styles/card.css

@ -47,3 +47,20 @@ h2.card-title {
object-fit: cover; 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;
}

1
assets/styles/notice.css

@ -10,6 +10,5 @@
.notice.info { .notice.info {
background-color: rgba(95, 115, 85, 0.15); /* Light version of --color-primary */ 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 */ color: var(--color-text); /* Use theme text color for better contrast */
} }

2
src/Controller/ArticleController.php

@ -125,7 +125,7 @@ class ArticleController extends AbstractController
$author = $redisCacheService->getMetadata($npub); $author = $redisCacheService->getMetadata($npub);
return $this->render('Pages/article.html.twig', [ return $this->render('pages/article.html.twig', [
'article' => $article, 'article' => $article,
'author' => $author, 'author' => $author,
'npub' => $npub, 'npub' => $npub,

89
src/Controller/DefaultController.php

@ -60,10 +60,10 @@ class DefaultController extends AbstractController
}); });
$list = []; $list = [];
$slugs = [];
$coordinates = []; // Store full coordinates (kind:author:slug) $coordinates = []; // Store full coordinates (kind:author:slug)
$category = []; $category = [];
// Extract category metadata and article coordinates
foreach ($catIndex->getTags() as $tag) { foreach ($catIndex->getTags() as $tag) {
if ($tag[0] === 'title') { if ($tag[0] === 'title') {
$category['title'] = $tag[1]; $category['title'] = $tag[1];
@ -72,74 +72,77 @@ class DefaultController extends AbstractController
$category['summary'] = $tag[1]; $category['summary'] = $tag[1];
} }
if ($tag[0] === 'a') { if ($tag[0] === 'a') {
$parts = explode(':', $tag[1]); $coordinates[] = $tag[1]; // Store the full coordinate
if (count($parts) === 3) {
$slugs[] = $parts[2];
$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)); $query = new Terms('slug', array_values($slugs));
$articles = $finder->find($query); $articles = $finder->find($query);
// Create a map of slug => item to remove duplicates // Create a map of slug => item to remove duplicates
$slugMap = []; $slugMap = [];
foreach ($articles as $item) { foreach ($articles as $item) {
$slug = $item->getSlug(); $slug = $item->getSlug();
if ($slug !== '') {
if ($slug !== '' && !isset($slugMap[$slug])) {
$slugMap[$slug] = $item; $slugMap[$slug] = $item;
} }
} }
// Find missing articles based on coordinates // Find missing coordinates
$missingCoordinates = []; $missingCoordinates = [];
$missingIndexes = []; foreach ($coordinates as $coordinate) {
$parts = explode(':', $coordinate);
for ($i = 0; $i < count($slugs); $i++) { if (count($parts) === 3 && !isset($slugMap[$parts[2]])) {
$slug = $slugs[$i]; $missingCoordinates[] = $coordinate;
if (!isset($slugMap[$slug])) {
$missingCoordinates[] = $coordinates[$i];
$missingIndexes[$coordinates[$i]] = $i; // Track original position
} }
} }
// If we have missing articles, fetch them from nostr // If we have missing articles, fetch them directly using NostrClient's getArticlesByCoordinates
if (!empty($missingCoordinates)) { if (!empty($missingCoordinates)) {
$logger->info('Fetching missing articles', [
$logger->info('There were missing articles', [
'missing' => $missingCoordinates 'missing' => $missingCoordinates
]); ]);
try { // try {
$nostrArticles = $nostrClient->getArticlesByCoordinates($missingCoordinates); // $nostrArticles = $nostrClient->getArticlesByCoordinates($missingCoordinates);
//
foreach ($nostrArticles as $coordinate => $event) { // foreach ($nostrArticles as $coordinate => $event) {
$parts = explode(':', $coordinate); // $parts = explode(':', $coordinate);
if (count($parts) === 3) { // if (count($parts) === 3) {
$article = $articleFactory->createFromLongFormContentEvent($event); // $article = $articleFactory->createFromLongFormContentEvent($event);
// // Save article to database for future queries
// Add to the slugMap // $nostrClient->saveEachArticleToTheDatabase($article);
$slugMap[$article->getSlug()] = $article; // // Add to the slugMap
} // $slugMap[$article->getSlug()] = $article;
} // }
} catch (\Exception $e) { // }
$logger->error('Error fetching missing articles', [ // } catch (\Exception $e) {
'error' => $e->getMessage() // $logger->error('Error fetching missing articles', [
]); // 'error' => $e->getMessage()
} // ]);
// }
} }
// Reorder by the original $slugs to maintain order // Build ordered list based on original coordinates order
$results = []; foreach ($coordinates as $coordinate) {
foreach ($slugs as $slug) { $parts = explode(':', $coordinate);
if (isset($slugMap[$slug])) { if (count($parts) === 3 && isset($slugMap[$parts[2]])) {
$results[] = $slugMap[$slug]; $list[] = $slugMap[$parts[2]];
} }
} }
$list = array_values($results);
} }
return $this->render('pages/category.html.twig', [ return $this->render('pages/category.html.twig', [

1
src/Enum/KindsEnum.php

@ -16,6 +16,7 @@ enum KindsEnum: int
case LONGFORM = 30023; // NIP-23 case LONGFORM = 30023; // NIP-23
case LONGFORM_DRAFT = 30024; // NIP-23 case LONGFORM_DRAFT = 30024; // NIP-23
case PUBLICATION_INDEX = 30040; case PUBLICATION_INDEX = 30040;
case ZAP = 9735; // NIP-57, Zaps
case HIGHLIGHTS = 9802; case HIGHLIGHTS = 9802;
case RELAY_LIST = 10002; // NIP-65, Relay list metadata case RELAY_LIST = 10002; // NIP-65, Relay list metadata
case APP_DATA = 30078; // NIP-78, Arbitrary custom app data case APP_DATA = 30078; // NIP-78, Arbitrary custom app data

101
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 * @throws \Exception
*/ */
public function getComments($coordinate): array public function getComments(string $coordinate): array
{ {
$list = []; $this->logger->info('Getting comments for coordinate', ['coordinate' => $coordinate]);
$parts = explode(':', $coordinate);
$subscription = new Subscription(); // Get author from coordinate, then relays
$subscriptionId = $subscription->setId(); $parts = explode(':', $coordinate);
$filter = new Filter(); if (count($parts) !== 3) {
$filter->setKinds([KindsEnum::COMMENTS, KindsEnum::TEXT_NOTE]); throw new \InvalidArgumentException('Invalid coordinate format, expected kind:pubkey:identifier');
$filter->setTag('#a', [$coordinate]); }
$requestMessage = new RequestMessage($subscriptionId, [$filter]); $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(); // Process the response and deduplicate by eventId
// response is an array of arrays $uniqueEvents = [];
foreach ($response as $value) { $this->processResponse($request->send(), function($event) use (&$uniqueEvents, $pubkey) {
foreach ($value as $item) { $this->logger->debug('Received comment event', ['event_id' => $event->id]);
switch ($item->type) { // If event has p tag with the pubkey, it's a comment
case 'EVENT': // Loop tags, look for 'p' tag
dump($item); foreach ($event->tags as $tag) {
$list[] = $item; if ($tag[0] === 'p' && $tag[1] === $pubkey) {
break; $uniqueEvents[$event->id] = $event;
case 'AUTH': break;
// throw new UnauthorizedHttpException('', 'Relay requires authentication');
case 'ERROR':
case 'NOTICE':
// throw new \Exception('An error occurred');
default:
// nothing to do here
} }
} }
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;
});
} }
/** /**

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

@ -1,7 +1,13 @@
<div class="comments"> <div class="comments">
{% for item in list %} {% for item in list %}
<div class="comment"> <div class="card comment">
<span>{{ item.content }}</span> <div class="metadata">
<p><twig:Molecules:UserFromNpub ident="{{ item.pubkey }}" /></p>
<small>{{ item.created_at|date('F j Y') }}</small>
</div>
<div class="card-body">
<p>{{ item.content }}</p>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

8
templates/pages/article.html.twig

@ -58,19 +58,23 @@
{{ content|raw }} {{ content|raw }}
</div> </div>
{% if article.topics|length > 0 %}
<hr class="divider" /> <hr class="divider" />
<div class="tags"> <div class="tags">
{% for tag in article.topics %} {% for tag in article.topics %}
<span class="tag">{{ tag }}</span> <span class="tag">{{ tag }}</span>
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
</div> </div>
{# <pre>#} <hr class="divider" />
{# <pre>#}
{# {{ article.content }}#} {# {{ article.content }}#}
{# </pre>#} {# </pre>#}
<twig:Organisms:Comments current="30032:{{ article.pubkey }}:{{ article.slug }}"></twig:Organisms:Comments> <twig:Organisms:Comments current="30023:{{ article.pubkey }}:{{ article.slug }}"></twig:Organisms:Comments>
{% endblock %} {% endblock %}
{% block aside %} {% block aside %}

Loading…
Cancel
Save