Browse Source

Simplify

imwald
Nuša Pukšič 3 months ago
parent
commit
19c49ba550
  1. 2
      config/packages/cache.yaml
  2. 16
      src/Controller/ArticleController.php
  3. 23
      src/Controller/AuthorController.php
  4. 81
      src/Service/NostrClient.php
  5. 49
      src/Service/RedisCacheService.php
  6. 149
      src/Util/CommonMark/Converter.php
  7. 4
      templates/pages/article.html.twig
  8. 181
      templates/pages/author-about.html.twig
  9. 4
      templates/pages/author-media.html.twig
  10. 3
      templates/pages/author.html.twig

2
config/packages/cache.yaml

@ -19,7 +19,7 @@ framework:
articles.cache: articles.cache:
adapter: cache.adapter.redis adapter: cache.adapter.redis
provider: Redis provider: Redis
default_lifetime: 3600 default_lifetime: 0
npub.cache: npub.cache:
adapter: cache.adapter.redis adapter: cache.adapter.redis
provider: Redis provider: Redis

16
src/Controller/ArticleController.php

@ -97,12 +97,14 @@ class ArticleController extends AbstractController
$article = $articles[0]; $article = $articles[0];
$cacheKey = 'article_' . $article->getEventId(); $parsed = $converter->convertToHTML($article->getContent());
$cacheItem = $articlesCache->getItem($cacheKey);
if (!$cacheItem->isHit()) { // $cacheKey = 'article_' . $article->getEventId();
$cacheItem->set($converter->convertToHTML($article->getContent())); // $cacheItem = $articlesCache->getItem($cacheKey);
$articlesCache->save($cacheItem); // if (!$cacheItem->isHit()) {
} // $cacheItem->set($converter->convertToHTML($article->getContent()));
// $articlesCache->save($cacheItem);
// }
$key = new Key(); $key = new Key();
$npub = $key->convertPublicKeyToBech32($article->getPubkey()); $npub = $key->convertPublicKeyToBech32($article->getPubkey());
@ -126,7 +128,7 @@ class ArticleController extends AbstractController
'article' => $article, 'article' => $article,
'author' => $author, 'author' => $author,
'npub' => $npub, 'npub' => $npub,
'content' => $cacheItem->get(), 'content' => $parsed, //$cacheItem->get(),
'canEdit' => $canEdit, 'canEdit' => $canEdit,
'canonical' => $canonical 'canonical' => $canonical
]); ]);

23
src/Controller/AuthorController.php

@ -94,29 +94,6 @@ class AuthorController extends AbstractController
]); ]);
} }
/**
* @throws Exception
*/
#[Route('/p/{npub}/about', name: 'author-about', requirements: ['npub' => '^npub1.*'])]
public function about($npub, RedisCacheService $redisCacheService): Response
{
$keys = new Key();
$pubkey = $keys->convertToHex($npub);
// Get metadata with raw event for debugging
$profileData = $redisCacheService->getMetadataWithRawEvent($npub);
$author = $profileData['metadata'];
$rawEvent = $profileData['rawEvent'];
return $this->render('pages/author-about.html.twig', [
'author' => $author,
'npub' => $npub,
'pubkey' => $pubkey,
'rawEvent' => $rawEvent,
'is_author_profile' => true,
]);
}
/** /**
* @throws Exception * @throws Exception
* @throws ExceptionInterface * @throws ExceptionInterface

81
src/Service/NostrClient.php

@ -108,6 +108,7 @@ class NostrClient
{ {
$relaySet = $this->defaultRelaySet; $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 ); $this->logger->info('Getting metadata for pubkey ' . $pubkey );
$request = $this->createNostrRequest( $request = $this->createNostrRequest(
kinds: [KindsEnum::METADATA], kinds: [KindsEnum::METADATA],
@ -131,45 +132,6 @@ class NostrClient
return $events[0]; return $events[0];
} }
public function getNpubLongForm($npub): void
{
$subscription = new Subscription();
$subscriptionId = $subscription->setId();
$filter = new Filter();
$filter->setKinds([KindsEnum::LONGFORM]);
$filter->setAuthors([$npub]);
$filter->setSince(strtotime('-6 months')); // too much?
$requestMessage = new RequestMessage($subscriptionId, [$filter]);
// if user is logged in, use their settings
/* @var $user */
$user = $this->tokenStorage->getToken()?->getUser();
$relays = $this->defaultRelaySet;
if ($user && $user->getRelays()) {
$relays = new RelaySet();
foreach ($user->getRelays() as $relayArr) {
if ($relayArr[2] == 'write') {
$relays->addRelay(new Relay($relayArr[1]));
}
}
}
$request = new Request($relays, $requestMessage);
$response = $request->send();
// response is an n-dimensional array, where n is the number of relays in the set
// check that response has events in the results
foreach ($response as $relayRes) {
$filtered = array_filter($relayRes, function ($item) {
return $item->type === 'EVENT';
});
if (count($filtered) > 0) {
$this->saveLongFormContent($filtered);
}
}
// TODO handle relays that require auth
}
public function publishEvent(Event $event, array $relays): array public function publishEvent(Event $event, array $relays): array
{ {
$eventMessage = new EventMessage($event); $eventMessage = new EventMessage($event);
@ -323,6 +285,47 @@ class NostrClient
return $events[0]; return $events[0];
} }
/**
* Get multiple events by their IDs
*
* @param array $eventIds Array of event IDs
* @param array $relays Optional array of relay URLs to query
* @return array Array of events indexed by ID
* @throws \Exception
*/
public function getEventsByIds(array $eventIds, array $relays = []): array
{
if (empty($eventIds)) {
return [];
}
$this->logger->info('Getting events by IDs', ['event_ids' => $eventIds, 'relays' => $relays]);
// Use provided relays or default if empty
$relaySet = empty($relays) ? $this->defaultRelaySet : $this->createRelaySet($relays);
// Create request using the helper method
$request = $this->createNostrRequest(
kinds: [],
filters: ['ids' => $eventIds],
relaySet: $relaySet
);
// Process the response
$events = $this->processResponse($request->send(), function($event) {
$this->logger->debug('Received event', ['event' => $event]);
return $event;
});
// Index events by ID
$eventsMap = [];
foreach ($events as $event) {
$eventsMap[$event->id] = $event;
}
return $eventsMap;
}
/** /**
* Fetch event by naddr * Fetch event by naddr
* *

49
src/Service/RedisCacheService.php

@ -7,6 +7,7 @@ use App\Entity\Event;
use App\Enum\KindsEnum; use App\Enum\KindsEnum;
use App\Util\NostrKeyUtil; use App\Util\NostrKeyUtil;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException; use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -55,7 +56,7 @@ readonly class RedisCacheService
$rawEvent = $this->fetchRawUserEvent($pubkey); $rawEvent = $this->fetchRawUserEvent($pubkey);
return $this->parseUserMetadata($rawEvent, $pubkey); return $this->parseUserMetadata($rawEvent, $pubkey);
}); });
} catch (InvalidArgumentException $e) { } catch (Exception|InvalidArgumentException $e) {
$this->logger->error('Error getting user data.', ['exception' => $e]); $this->logger->error('Error getting user data.', ['exception' => $e]);
} }
// If content is still default, delete cache to retry next time // If content is still default, delete cache to retry next time
@ -73,6 +74,7 @@ readonly class RedisCacheService
/** /**
* Fetch raw user event from Nostr client, with error fallback. * Fetch raw user event from Nostr client, with error fallback.
* @param string $pubkey Hex-encoded public key * @param string $pubkey Hex-encoded public key
* @throws Exception
*/ */
private function fetchRawUserEvent(string $pubkey): \stdClass private function fetchRawUserEvent(string $pubkey): \stdClass
{ {
@ -80,12 +82,8 @@ readonly class RedisCacheService
return $this->nostrClient->getPubkeyMetadata($pubkey); return $this->nostrClient->getPubkeyMetadata($pubkey);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error('Error getting user data.', ['exception' => $e]); $this->logger->error('Error getting user data.', ['exception' => $e]);
$rawEvent = new \stdClass(); // Rethrow exception to be caught in getMetadata
$rawEvent->content = json_encode([ throw $e;
'name' => substr($pubkey, 0, 8) . '…' . substr($pubkey, -4)
]);
$rawEvent->tags = [];
return $rawEvent;
} }
} }
@ -131,43 +129,6 @@ readonly class RedisCacheService
return $contentData; return $contentData;
} }
/**
* Get metadata with raw event for debugging purposes.
*
* @param string $pubkey Hex-encoded public key
* @return array{metadata: \stdClass, rawEvent: \stdClass}
* @throws InvalidArgumentException
*/
public function getMetadataWithRawEvent(string $pubkey): array
{
if (!NostrKeyUtil::isHexPubkey($pubkey)) {
throw new \InvalidArgumentException('getMetadataWithRawEvent expects hex pubkey');
}
$cacheKey = '0_with_raw_' . $pubkey;
try {
return $this->npubCache->get($cacheKey, function (ItemInterface $item) use ($pubkey) {
$item->expiresAfter(3600); // 1 hour, adjust as needed
$rawEvent = $this->fetchRawUserEvent($pubkey);
$contentData = $this->parseUserMetadata($rawEvent, $pubkey);
return [
'metadata' => $contentData,
'rawEvent' => $rawEvent
];
});
} catch (InvalidArgumentException $e) {
$this->logger->error('Error getting user data with raw event.', ['exception' => $e]);
$content = new \stdClass();
$content->name = substr($pubkey, 0, 8) . '…' . substr($pubkey, -4);
$rawEvent = new \stdClass();
$rawEvent->content = json_encode($content);
$rawEvent->tags = [];
return [
'metadata' => $content,
'rawEvent' => $rawEvent
];
}
}
/** /**
* Fetch metadata for multiple pubkeys at once using Redis getItems. * Fetch metadata for multiple pubkeys at once using Redis getItems.
* Falls back to getMetadata for cache misses. * Falls back to getMetadata for cache misses.

149
src/Util/CommonMark/Converter.php

@ -24,6 +24,12 @@ use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\MarkdownConverter; use League\CommonMark\MarkdownConverter;
use League\CommonMark\Renderer\HtmlDecorator; use League\CommonMark\Renderer\HtmlDecorator;
use Twig\Environment as TwigEnvironment; use Twig\Environment as TwigEnvironment;
use nostriphant\NIP19\Bech32;
use nostriphant\NIP19\Data\NAddr;
use nostriphant\NIP19\Data\NEvent;
use nostriphant\NIP19\Data\Note;
use nostriphant\NIP19\Data\NProfile;
use nostriphant\NIP19\Data\NPub;
readonly class Converter readonly class Converter
{ {
@ -39,6 +45,9 @@ readonly class Converter
*/ */
public function convertToHTML(string $markdown): string public function convertToHTML(string $markdown): string
{ {
// Preprocess nostr: links for batching
$markdown = $this->preprocessNostrLinks($markdown);
// Check if the article has more than three headings // Check if the article has more than three headings
// Match all headings (from level 1 to 6) // Match all headings (from level 1 to 6)
preg_match_all('/^#+\s.*$/m', $markdown, $matches); preg_match_all('/^#+\s.*$/m', $markdown, $matches);
@ -59,7 +68,7 @@ readonly class Converter
], ],
'embed' => [ 'embed' => [
'adapter' => new OscaroteroEmbedAdapter(), // See the "Adapter" documentation below 'adapter' => new OscaroteroEmbedAdapter(), // See the "Adapter" documentation below
'allowed_domains' => ['youtube.com', 'twitter.com', 'github.com'], 'allowed_domains' => ['youtube.com', 'x.com', 'github.com', 'fountain.fm'],
'fallback' => 'link' 'fallback' => 'link'
], ],
]; ];
@ -69,8 +78,6 @@ readonly class Converter
$environment->addExtension(new FootnoteExtension()); $environment->addExtension(new FootnoteExtension());
$environment->addExtension(new TableExtension()); $environment->addExtension(new TableExtension());
$environment->addExtension(new StrikethroughExtension()); $environment->addExtension(new StrikethroughExtension());
// create a custom extension, that handles nostr mentions
$environment->addExtension(new NostrSchemeExtension($this->redisCacheService, $this->nostrClient, $this->twig, $this->nostrKeyUtil));
$environment->addExtension(new SmartPunctExtension()); $environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new EmbedExtension()); $environment->addExtension(new EmbedExtension());
$environment->addRenderer(Embed::class, new HtmlDecorator(new EmbedRenderer(), 'div', ['class' => 'embedded-content'])); $environment->addRenderer(Embed::class, new HtmlDecorator(new EmbedRenderer(), 'div', ['class' => 'embedded-content']));
@ -88,4 +95,140 @@ readonly class Converter
return $converter->convert($content); return $converter->convert($content);
} }
private function preprocessNostrLinks(string $markdown): string
{
// Find all nostr: links
preg_match_all('/nostr:(?:npub1|nprofile1|note1|nevent1|naddr1)[^\\s<>()\\[\\]{}"\'`.,;:!?]*/', $markdown, $matches);
if (empty($matches[0])) {
return $markdown;
}
$links = array_unique($matches[0]);
$replacements = [];
// Collect data for batching
$pubkeys = [];
$eventIds = [];
foreach ($links as $link) {
$bechEncoded = substr($link, 6); // Remove "nostr:"
try {
$decoded = new Bech32($bechEncoded);
switch ($decoded->type) {
case 'npub':
/** @var NPub $object */
$object = $decoded->data;
$hex = $this->nostrKeyUtil->npubToHex($bechEncoded);
$pubkeys[$hex] = $bechEncoded;
break;
case 'nprofile':
/** @var NProfile $object */
$object = $decoded->data;
$pubkeys[$object->pubkey] = $bechEncoded;
break;
case 'note':
/** @var Note $object */
$object = $decoded->data;
$eventIds[$object->data] = $bechEncoded;
break;
case 'nevent':
/** @var NEvent $object */
$object = $decoded->data;
$eventIds[$object->id] = $bechEncoded;
break;
case 'naddr':
// For naddr, we might need to fetch the event, but for now, handle as simple link
break;
}
} catch (\Exception $e) {
// Invalid link, skip
continue;
}
}
// 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)) {
try {
$events = $this->nostrClient->getEventsByIds(array_keys($eventIds));
} catch (\Exception $e) {
// If batch fails, events remain empty
}
}
// Now, render each link
foreach ($links as $link) {
$bechEncoded = substr($link, 6);
try {
$decoded = new Bech32($bechEncoded);
$html = $this->renderNostrLink($decoded, $bechEncoded, $metadata, $events);
$replacements[$link] = $html;
} catch (\Exception $e) {
// Keep original link if error
$replacements[$link] = $link;
}
}
// Replace in markdown
return str_replace(array_keys($replacements), array_values($replacements), $markdown);
}
private function renderNostrLink(Bech32 $decoded, string $bechEncoded, array $metadata, array $events): string
{
switch ($decoded->type) {
case 'npub':
$hex = $this->nostrKeyUtil->npubToHex($bechEncoded);
$profile = $metadata[$hex] ?? null;
if ($profile && isset($profile->name)) {
return '<a href="/profile/' . $bechEncoded . '" class="nostr-mention">@' . htmlspecialchars($profile->name) . '</a>';
} else {
return '<a href="/profile/' . $bechEncoded . '" class="nostr-mention">@' . substr($bechEncoded, 5, 8) . '…</a>';
}
case 'nprofile':
$object = $decoded->data;
return '<a href="/profile/' . $bechEncoded . '" class="nostr-mention">@' . substr($bechEncoded, 9, 8) . '…</a>';
case 'note':
$object = $decoded->data;
$event = $events[$object->data] ?? null;
if ($event && $event->kind === 20) {
$pictureCardHtml = $this->twig->render('/event/_kind20_picture.html.twig', [
'event' => $event,
'embed' => true
]);
return $pictureCardHtml;
} else {
return '<a href="/event/' . $bechEncoded . '" class="nostr-link">' . $bechEncoded . '</a>';
}
case 'nevent':
$object = $decoded->data;
$event = $events[$object->id] ?? null;
if ($event) {
$authorMetadata = $metadata[$event->pubkey] ?? null;
$eventCardHtml = $this->twig->render('components/event_card.html.twig', [
'event' => $event,
'author' => $authorMetadata,
'nevent' => $bechEncoded
]);
return $eventCardHtml;
} else {
return '<a href="/event/' . $bechEncoded . '" class="nostr-link">' . $bechEncoded . '</a>';
}
case 'naddr':
return '<a href="/event/' . $bechEncoded . '" class="nostr-link">' . $bechEncoded . '</a>';
default:
return $bechEncoded;
}
}
} }

4
templates/pages/article.html.twig

@ -16,6 +16,10 @@
<twig:Organisms:MagazineHero :mag="mag" :magazine="magazine" /> <twig:Organisms:MagazineHero :mag="mag" :magazine="magazine" />
{% endif %} {% endif %}
<pre>
{{ article.topics|json_encode(constant('JSON_PRETTY_PRINT')) }}
</pre>
<div class="article-actions"> <div class="article-actions">
{% if canEdit %} {% if canEdit %}
<a class="btn btn-primary" href="{{ path('editor-edit-slug', {'slug': article.slug}) }}">Edit article</a> <a class="btn btn-primary" href="{{ path('editor-edit-slug', {'slug': article.slug}) }}">Edit article</a>

181
templates/pages/author-about.html.twig

@ -1,181 +0,0 @@
{% extends 'layout.html.twig' %}
{% block body %}
{% include 'partial/_author-section.html.twig' with {author: author, npub: npub} %}
<section>
<div class="profile-tabs">
<a href="{{ path('author-profile', {'npub': npub}) }}" class="tab-link">Articles</a>
<a href="{{ path('author-media', {'npub': npub}) }}" class="tab-link">Media</a>
<a href="{{ path('author-about', {'npub': npub}) }}" class="tab-link active">About</a>
</div>
<div class="w-container mt-4">
<div class="profile-details">
<h2>Profile Information</h2>
{% if author.about is defined %}
<div class="profile-field">
<h3>About</h3>
<div class="profile-value">
{{ author.about|markdown_to_html|mentionify|linkify }}
</div>
</div>
{% endif %}
{% if author.banner is defined %}
<div class="profile-field">
<h3>Banner</h3>
<div class="profile-value">
<img src="{{ author.banner }}" alt="Profile banner" style="max-width: 100%; height: auto;" onerror="this.style.display = 'none'" />
</div>
</div>
{% endif %}
{% if author.website is defined %}
<div class="profile-field">
<h3>Website</h3>
<div class="profile-value">
<a href="{{ author.website }}" target="_blank" rel="noopener noreferrer">{{ author.website }}</a>
</div>
</div>
{% endif %}
{% if author.lud16 is defined %}
<div class="profile-field">
<h3>Lightning Address{{ author.lud16 is iterable and author.lud16|length > 1 ? 'es' : '' }}</h3>
<div class="profile-value">
{% if author.lud16 is iterable %}
{% for address in author.lud16 %}
<div class="mb-1">
<code>{{ address }}</code>
</div>
{% endfor %}
{% else %}
<code>{{ author.lud16 }}</code>
{% endif %}
</div>
</div>
{% endif %}
{% if author.lud06 is defined %}
<div class="profile-field">
<h3>LNURL{{ author.lud06 is iterable and author.lud06|length > 1 ? 's' : '' }}</h3>
<div class="profile-value">
{% if author.lud06 is iterable %}
{% for lnurl in author.lud06 %}
<div class="mb-1">
<code style="word-break: break-all;">{{ lnurl }}</code>
</div>
{% endfor %}
{% else %}
<code style="word-break: break-all;">{{ author.lud06 }}</code>
{% endif %}
</div>
</div>
{% endif %}
<div class="profile-field">
<h3>Public Key (hex)</h3>
<div class="profile-value">
<code style="word-break: break-all;">{{ pubkey }}</code>
</div>
</div>
<div class="profile-field">
<h3>Public Key (npub)</h3>
<div class="profile-value">
<code style="word-break: break-all;">{{ npub }}</code>
</div>
</div>
{# Display any additional fields that might be present #}
{% set standardFields = ['name', 'display_name', 'about', 'picture', 'banner', 'nip05', 'website', 'lud16', 'lud06', 'image'] %}
{% for key, value in author %}
{% if key not in standardFields and value is not empty %}
<div class="profile-field">
<h3>{{ key|title }}</h3>
<div class="profile-value">
{% if value starts with 'http://' or value starts with 'https://' %}
<a href="{{ value }}" target="_blank" rel="noopener noreferrer">{{ value }}</a>
{% else %}
{{ value }}
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{# Raw Event Debug Section #}
<div class="mt-6 p-4 bg-gray-100 rounded">
<details>
<summary class="cursor-pointer font-semibold text-lg mb-2">Raw Profile Event (Debug)</summary>
<div class="mt-2">
<h4 class="font-semibold">Event ID:</h4>
<pre class="bg-white p-2 rounded overflow-x-auto"><code>{{ rawEvent.id ?? 'N/A' }}</code></pre>
<h4 class="font-semibold mt-3">Created At:</h4>
<pre class="bg-white p-2 rounded overflow-x-auto"><code>{{ rawEvent.created_at ?? 'N/A' }} ({{ rawEvent.created_at is defined ? rawEvent.created_at|date('Y-m-d H:i:s') : 'N/A' }})</code></pre>
<h4 class="font-semibold mt-3">Tags:</h4>
<pre class="bg-white p-2 rounded overflow-x-auto"><code>{{ rawEvent.tags is defined ? rawEvent.tags|json_encode(constant('JSON_PRETTY_PRINT')) : '[]' }}</code></pre>
<h4 class="font-semibold mt-3">Content (JSON):</h4>
<pre class="bg-white p-2 rounded overflow-x-auto"><code>{{ rawEvent.content ?? '{}' }}</code></pre>
<h4 class="font-semibold mt-3">Signature:</h4>
<pre class="bg-white p-2 rounded overflow-x-auto text-xs"><code>{{ rawEvent.sig ?? 'N/A' }}</code></pre>
<h4 class="font-semibold mt-3">Full Event Object:</h4>
<pre class="bg-white p-2 rounded overflow-x-auto text-xs"><code>{{ rawEvent|json_encode(constant('JSON_PRETTY_PRINT')) }}</code></pre>
</div>
</details>
</div>
</div>
</section>
<style>
.profile-details {
max-width: 800px;
}
.profile-field {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e5e7eb;
}
.profile-field:last-child {
border-bottom: none;
}
.profile-field h3 {
font-size: 0.875rem;
font-weight: 600;
color: #6b7280;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.profile-value {
color: #1f2937;
word-wrap: break-word;
}
.profile-value code {
background-color: #f3f4f6;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
}
details summary {
user-select: none;
}
details[open] summary {
margin-bottom: 1rem;
}
pre {
white-space: pre-wrap;
word-wrap: break-word;
}
</style>
{% endblock %}

4
templates/pages/author-media.html.twig

@ -8,9 +8,7 @@
<div class="profile-tabs"> <div class="profile-tabs">
<a href="{{ path('author-profile', {'npub': npub}) }}" class="tab-link">Articles</a> <a href="{{ path('author-profile', {'npub': npub}) }}" class="tab-link">Articles</a>
<a href="{{ path('author-media', {'npub': npub}) }}" class="tab-link active">Media</a> <a href="{{ path('author-media', {'npub': npub}) }}" class="tab-link active">Media</a>
{% if is_granted('ROLE_ADMIN') %} </div>
<a href="{{ path('author-about', {'npub': npub}) }}" class="tab-link">About</a>
{% endif %} </div>
<div class="w-container"> <div class="w-container">
{% if pictureEvents|length > 0 %} {% if pictureEvents|length > 0 %}

3
templates/pages/author.html.twig

@ -8,9 +8,6 @@
<div class="profile-tabs"> <div class="profile-tabs">
<a href="{{ path('author-profile', {'npub': npub}) }}" class="tab-link active">Articles</a> <a href="{{ path('author-profile', {'npub': npub}) }}" class="tab-link active">Articles</a>
<a href="{{ path('author-media', {'npub': npub}) }}" class="tab-link">Media</a> <a href="{{ path('author-media', {'npub': npub}) }}" class="tab-link">Media</a>
{% if is_granted('ROLE_ADMIN') %}
<a href="{{ path('author-about', {'npub': npub}) }}" class="tab-link">About</a>
{% endif %}
</div> </div>
<div class="w-container" data-controller="author-articles" data-author-articles-pubkey-value="{{ pubkey }}" data-author-articles-hub-url-value="{{ mercure_public_hub_url }}"> <div class="w-container" data-controller="author-articles" data-author-articles-pubkey-value="{{ pubkey }}" data-author-articles-hub-url-value="{{ mercure_public_hub_url }}">

Loading…
Cancel
Save