26 changed files with 830 additions and 80 deletions
@ -0,0 +1,21 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Dto; |
||||||
|
|
||||||
|
/** |
||||||
|
* Nostr "⋯" share menu: copy npub; copy nevent or naddr (Jumble uses the same bech in /feed/notes/…). |
||||||
|
* Addressable (NIP-33) long-form / index events: prefer naddr; one-off stateless events: nevent. |
||||||
|
*/ |
||||||
|
final class NostrShareMenuContext |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
/** NIP-19 npub. Null only in rare fallbacks. */ |
||||||
|
public ?string $npub, |
||||||
|
public ?string $neventBech32, |
||||||
|
public ?string $naddrBech32, |
||||||
|
public string $jumbleHref, |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,73 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Nostr; |
||||||
|
|
||||||
|
use App\Entity\Event; |
||||||
|
use nostriphant\NIP19\Bech32; |
||||||
|
|
||||||
|
/** |
||||||
|
* NIP-33 / NIP-19 helpers: naddr for parameterized replaceable events (kind:pubkey:d). |
||||||
|
*/ |
||||||
|
final class Nip19Addressable |
||||||
|
{ |
||||||
|
/** |
||||||
|
* NIP-33 replaceable kinds (30000–39999) use a `d` tag; encode as naddr, not nevent, for clients. |
||||||
|
*/ |
||||||
|
public static function isParameterizedReplaceableKind(int $kind): bool |
||||||
|
{ |
||||||
|
return $kind >= 30_000 && $kind < 40_000; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param array<int, mixed> $tagRows |
||||||
|
*/ |
||||||
|
public static function dTagFromTagRows(array $tagRows): ?string |
||||||
|
{ |
||||||
|
foreach ($tagRows as $row) { |
||||||
|
if (!\is_array($row) && !\is_object($row)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (\is_object($row)) { |
||||||
|
$row = (array) $row; |
||||||
|
} |
||||||
|
$row = array_values($row); |
||||||
|
if ($row === []) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (strtolower((string) ($row[0] ?? '')) === 'd' && isset($row[1])) { |
||||||
|
$d = (string) $row[1]; |
||||||
|
if ($d !== '') { |
||||||
|
return $d; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public static function dTagFromEventEntity(Event $e): ?string |
||||||
|
{ |
||||||
|
return self::dTagFromTagRows($e->getTags()); |
||||||
|
} |
||||||
|
|
||||||
|
public static function naddrBech32( |
||||||
|
int $kind, |
||||||
|
string $pubkeyHex, |
||||||
|
string $dIdentifier, |
||||||
|
array $relays = [], |
||||||
|
): string { |
||||||
|
$pubkeyHex = strtolower($pubkeyHex); |
||||||
|
if (64 !== \strlen($pubkeyHex) || !ctype_xdigit($pubkeyHex)) { |
||||||
|
throw new \InvalidArgumentException('Invalid pubkey hex for naddr.'); |
||||||
|
} |
||||||
|
|
||||||
|
return (string) Bech32::naddr( |
||||||
|
kind: $kind, |
||||||
|
pubkey: $pubkeyHex, |
||||||
|
identifier: $dIdentifier, |
||||||
|
relays: $relays, |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,52 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Service; |
||||||
|
|
||||||
|
use App\Entity\Article; |
||||||
|
use swentel\nostr\Key\Key; |
||||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Canonical /p/{npub}/d/{slug} links for long-form and helpers for templates. |
||||||
|
*/ |
||||||
|
final class NostrPathHelper |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
private readonly UrlGeneratorInterface $router, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public function npubFromPubkeyHex(string $pubkeyHex): string |
||||||
|
{ |
||||||
|
return (new Key())->convertPublicKeyToBech32($pubkeyHex); |
||||||
|
} |
||||||
|
|
||||||
|
public function articlePath(Article $article): string |
||||||
|
{ |
||||||
|
$slug = (string) ($article->getSlug() ?? ''); |
||||||
|
if ($slug === '' || $article->getPubkey() === null) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
$npub = $this->npubFromPubkeyHex((string) $article->getPubkey()); |
||||||
|
|
||||||
|
return $this->router->generate('article', [ |
||||||
|
'npub' => $npub, |
||||||
|
'slug' => $slug, |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
public function articleAbsoluteUrl(Article $article): string |
||||||
|
{ |
||||||
|
$slug = (string) ($article->getSlug() ?? ''); |
||||||
|
if ($slug === '' || $article->getPubkey() === null) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
return $this->router->generate('article', [ |
||||||
|
'npub' => $this->npubFromPubkeyHex((string) $article->getPubkey()), |
||||||
|
'slug' => $slug, |
||||||
|
], UrlGeneratorInterface::ABSOLUTE_URL); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,348 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Service; |
||||||
|
|
||||||
|
use App\Dto\NostrShareMenuContext; |
||||||
|
use App\Entity\Article; |
||||||
|
use App\Entity\Event; |
||||||
|
use App\Nostr\Nip19Addressable; |
||||||
|
use App\Repository\ArticleRepository; |
||||||
|
use nostriphant\NIP19\Bech32; |
||||||
|
use swentel\nostr\Key\Key; |
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire; |
||||||
|
use Symfony\Component\HttpFoundation\Request; |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolves the header Nostr share menu (npub; naddr for addressables, else nevent; Jumble /feed/notes/…). |
||||||
|
*/ |
||||||
|
final class NostrShareMenuBuilder |
||||||
|
{ |
||||||
|
public const string ATTR_NPUB = 'nostr_share_npub'; |
||||||
|
|
||||||
|
public const string ATTR_NEVENT_BECH32 = 'nostr_share_nevent_bech32'; |
||||||
|
|
||||||
|
public const string ATTR_NADDR_BECH32 = 'nostr_share_naddr_bech32'; |
||||||
|
|
||||||
|
public static function applyWireEventToRequest(Request $request, object $event, array $relayHints = []): void |
||||||
|
{ |
||||||
|
$pubkeyHex = strtolower((string) ($event->pubkey ?? '')); |
||||||
|
if (64 !== \strlen($pubkeyHex) || !ctype_xdigit($pubkeyHex)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
$key = new Key(); |
||||||
|
$request->attributes->set(self::ATTR_NPUB, $key->convertPublicKeyToBech32($pubkeyHex)); |
||||||
|
$kind = (int) ($event->kind ?? 0); |
||||||
|
$d = self::dTagFromWireEvent($event); |
||||||
|
if (Nip19Addressable::isParameterizedReplaceableKind($kind) && $d !== null) { |
||||||
|
$naddr = Nip19Addressable::naddrBech32($kind, $pubkeyHex, $d, $relayHints); |
||||||
|
$request->attributes->set(self::ATTR_NADDR_BECH32, $naddr); |
||||||
|
|
||||||
|
return; |
||||||
|
} |
||||||
|
$eventIdHex = strtolower((string) ($event->id ?? '')); |
||||||
|
if (64 === \strlen($eventIdHex) && ctype_xdigit($eventIdHex)) { |
||||||
|
$rebuilt = (string) Bech32::nevent( |
||||||
|
id: $eventIdHex, |
||||||
|
relays: $relayHints, |
||||||
|
author: $pubkeyHex, |
||||||
|
kind: $kind, |
||||||
|
); |
||||||
|
$request->attributes->set(self::ATTR_NEVENT_BECH32, $rebuilt); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<mixed>|\ArrayObject<int, mixed> $event->tags |
||||||
|
*/ |
||||||
|
private static function dTagFromWireEvent(object $event): ?string |
||||||
|
{ |
||||||
|
if (!isset($event->tags)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$rows = $event->tags; |
||||||
|
if ($rows instanceof \ArrayObject) { |
||||||
|
$rows = $rows->getArrayCopy(); |
||||||
|
} |
||||||
|
if (!\is_array($rows)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$norm = array_values( |
||||||
|
array_map( |
||||||
|
static function ($r) { |
||||||
|
if (!\is_array($r) && !\is_object($r)) { |
||||||
|
return $r; |
||||||
|
} |
||||||
|
if (\is_object($r)) { |
||||||
|
$r = (array) $r; |
||||||
|
} |
||||||
|
|
||||||
|
return $r; |
||||||
|
}, |
||||||
|
$rows |
||||||
|
) |
||||||
|
); |
||||||
|
|
||||||
|
return Nip19Addressable::dTagFromTagRows($norm); |
||||||
|
} |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
private readonly MagazineIndexStore $magazineIndexStore, |
||||||
|
private readonly ArticleRepository $articleRepository, |
||||||
|
#[Autowire('%npub%')] |
||||||
|
private readonly string $siteNpub, |
||||||
|
#[Autowire('%d_tag%')] |
||||||
|
private readonly string $rootDTag, |
||||||
|
#[Autowire('%jumble_profile_users_base%')] |
||||||
|
private readonly string $jumbleProfileUsersBase, |
||||||
|
#[Autowire('%jumble_feed_notes_base%')] |
||||||
|
private readonly string $jumbleFeedNotesBase, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
private function nostrKey(): Key |
||||||
|
{ |
||||||
|
return new Key(); |
||||||
|
} |
||||||
|
|
||||||
|
public function buildForRequest(Request $request): ?NostrShareMenuContext |
||||||
|
{ |
||||||
|
if ($request->isXmlHttpRequest() || 'xmlhttprequest' === strtolower((string) $request->headers->get('X-Requested-With'))) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
if ($request->attributes->getBoolean('_embed')) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$route = (string) $request->attributes->get('_route', ''); |
||||||
|
if (str_ends_with($route, 'fragment') || str_starts_with($request->getPathInfo(), '/fragment/')) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
if ('' === $route) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
|
||||||
|
return match ($route) { |
||||||
|
'home' => $this->siteWithRootMenu(), |
||||||
|
'article' => $this->forArticleNpubD( |
||||||
|
(string) $request->attributes->get('npub', ''), |
||||||
|
(string) $request->attributes->get('slug', ''), |
||||||
|
), |
||||||
|
'author-profile' => $this->forAuthorProfile($request->attributes->get('npub', '')), |
||||||
|
'nevent' => $this->forNevent($request, (string) $request->attributes->get('nevent', '')), |
||||||
|
'magazine-category' => $this->forCategory($request->attributes->get('slug', '')), |
||||||
|
'articles', 'featured_authors', 'search', 'article-preview', 'article-preview-event', 'editor-create', 'editor-edit' => $this->siteWithRootMenu(), |
||||||
|
default => $this->siteWithRootMenu(), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private function forArticleNpubD(string $npub, string $slug): NostrShareMenuContext |
||||||
|
{ |
||||||
|
if ($npub === '' || $slug === '' || !str_starts_with($npub, 'npub1')) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
$list = $this->articleRepository->findBy(['slug' => $slug], ['createdAt' => 'DESC'], 1); |
||||||
|
$article = $list[0] ?? null; |
||||||
|
if ($article === null) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
if ($this->nostrKey()->convertToHex($npub) !== strtolower((string) $article->getPubkey())) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->fromArticle($article); |
||||||
|
} |
||||||
|
|
||||||
|
private function fromArticle(Article $article): NostrShareMenuContext |
||||||
|
{ |
||||||
|
$npub = $this->nostrKey()->convertPublicKeyToBech32((string) $article->getPubkey()); |
||||||
|
$kind = (int) ($article->getKind()?->value ?? 30023); |
||||||
|
$d = (string) ($article->getSlug() ?? ''); |
||||||
|
if ($d === '') { |
||||||
|
return new NostrShareMenuContext( |
||||||
|
$npub, |
||||||
|
null, |
||||||
|
null, |
||||||
|
$this->profileJumbleUrl($npub), |
||||||
|
); |
||||||
|
} |
||||||
|
$pk = strtolower((string) $article->getPubkey()); |
||||||
|
$naddr = Nip19Addressable::naddrBech32($kind, $pk, $d, []); |
||||||
|
|
||||||
|
return new NostrShareMenuContext( |
||||||
|
$npub, |
||||||
|
null, |
||||||
|
$naddr, |
||||||
|
$this->feedJumble($naddr), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private function forAuthorProfile(mixed $npubParam): NostrShareMenuContext |
||||||
|
{ |
||||||
|
$npub = (string) $npubParam; |
||||||
|
if ($npub === '' || !str_starts_with($npub, 'npub1')) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
|
||||||
|
return new NostrShareMenuContext( |
||||||
|
$npub, |
||||||
|
null, |
||||||
|
null, |
||||||
|
$this->profileJumbleUrl($npub), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private function forNevent(Request $request, string $neventFromRoute): NostrShareMenuContext |
||||||
|
{ |
||||||
|
if ($request->attributes->has(self::ATTR_NPUB) && $request->attributes->has(self::ATTR_NADDR_BECH32)) { |
||||||
|
$naddr = (string) $request->attributes->get(self::ATTR_NADDR_BECH32); |
||||||
|
$np = (string) $request->attributes->get(self::ATTR_NPUB); |
||||||
|
|
||||||
|
return new NostrShareMenuContext( |
||||||
|
$np, |
||||||
|
null, |
||||||
|
$naddr, |
||||||
|
$this->feedJumble($naddr), |
||||||
|
); |
||||||
|
} |
||||||
|
if ($request->attributes->has(self::ATTR_NPUB) && $request->attributes->has(self::ATTR_NEVENT_BECH32)) { |
||||||
|
$nb = (string) $request->attributes->get(self::ATTR_NEVENT_BECH32); |
||||||
|
$np = (string) $request->attributes->get(self::ATTR_NPUB); |
||||||
|
|
||||||
|
return new NostrShareMenuContext( |
||||||
|
$np, |
||||||
|
$nb, |
||||||
|
null, |
||||||
|
$this->feedJumble($nb), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
$nevent = $neventFromRoute; |
||||||
|
if ($nevent === '' || !str_starts_with($nevent, 'nevent1')) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
try { |
||||||
|
$decoded = new Bech32($nevent); |
||||||
|
} catch (\Throwable) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
if ($decoded->type !== 'nevent' || !isset($decoded->data->id)) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
$eventId = strtolower((string) $decoded->data->id); |
||||||
|
if (64 !== \strlen($eventId) || !ctype_xdigit($eventId)) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
$authorHex = $decoded->data->author ?? null; |
||||||
|
if (\is_string($authorHex) && 64 === \strlen($authorHex) && ctype_xdigit($authorHex)) { |
||||||
|
$authorHex = strtolower($authorHex); |
||||||
|
} else { |
||||||
|
$authorHex = null; |
||||||
|
} |
||||||
|
$kind = isset($decoded->data->kind) ? (int) $decoded->data->kind : 1; |
||||||
|
$relays = $decoded->data->relays ?? []; |
||||||
|
$relays = \is_array($relays) ? $relays : []; |
||||||
|
if ($authorHex !== null) { |
||||||
|
$rebuilt = (string) Bech32::nevent( |
||||||
|
id: $eventId, |
||||||
|
relays: $relays, |
||||||
|
author: $authorHex, |
||||||
|
kind: $kind, |
||||||
|
); |
||||||
|
|
||||||
|
return new NostrShareMenuContext( |
||||||
|
$this->nostrKey()->convertPublicKeyToBech32($authorHex), |
||||||
|
$rebuilt, |
||||||
|
null, |
||||||
|
$this->feedJumble($rebuilt), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return new NostrShareMenuContext( |
||||||
|
null, |
||||||
|
$nevent, |
||||||
|
null, |
||||||
|
$this->feedJumble($nevent), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private function forCategory(string $slug): NostrShareMenuContext |
||||||
|
{ |
||||||
|
if ($slug === '') { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
$cat = $this->magazineIndexStore->getCategory($slug); |
||||||
|
if ($cat === null) { |
||||||
|
return $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
|
||||||
|
return $this->fromNostrEvent($cat) ?? $this->siteWithRootMenu(); |
||||||
|
} |
||||||
|
|
||||||
|
private function fromNostrEvent(Event $e): ?NostrShareMenuContext |
||||||
|
{ |
||||||
|
$id = strtolower($e->getId()); |
||||||
|
if (64 !== \strlen($id) || !ctype_xdigit($id)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$pk = strtolower($e->getPubkey()); |
||||||
|
if (64 !== \strlen($pk) || !ctype_xdigit($pk)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
$kind = (int) $e->getKind(); |
||||||
|
$d = Nip19Addressable::dTagFromEventEntity($e); |
||||||
|
$npub = $this->nostrKey()->convertPublicKeyToBech32($pk); |
||||||
|
if (Nip19Addressable::isParameterizedReplaceableKind($kind) && $d !== null) { |
||||||
|
$naddr = Nip19Addressable::naddrBech32($kind, $pk, $d, []); |
||||||
|
|
||||||
|
return new NostrShareMenuContext( |
||||||
|
$npub, |
||||||
|
null, |
||||||
|
$naddr, |
||||||
|
$this->feedJumble($naddr), |
||||||
|
); |
||||||
|
} |
||||||
|
$nevent = (string) Bech32::nevent( |
||||||
|
id: $id, |
||||||
|
relays: [], |
||||||
|
author: $pk, |
||||||
|
kind: $kind, |
||||||
|
); |
||||||
|
|
||||||
|
return new NostrShareMenuContext( |
||||||
|
$npub, |
||||||
|
$nevent, |
||||||
|
null, |
||||||
|
$this->feedJumble($nevent), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private function siteWithRootMenu(): NostrShareMenuContext |
||||||
|
{ |
||||||
|
$root = $this->magazineIndexStore->getRoot($this->siteNpub, $this->rootDTag); |
||||||
|
if (null === $fromRoot = $root ? $this->fromNostrEvent($root) : null) { |
||||||
|
return new NostrShareMenuContext( |
||||||
|
$this->siteNpub, |
||||||
|
null, |
||||||
|
null, |
||||||
|
$this->profileJumbleUrl($this->siteNpub), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return $fromRoot; |
||||||
|
} |
||||||
|
|
||||||
|
private function profileJumbleUrl(string $npub): string |
||||||
|
{ |
||||||
|
$b = rtrim($this->jumbleProfileUsersBase, '/'); |
||||||
|
|
||||||
|
return $b === '' ? '#' : $b.'/'.$npub; |
||||||
|
} |
||||||
|
|
||||||
|
private function feedJumble(string $naddrOrNeventOrNoteBech32): string |
||||||
|
{ |
||||||
|
$b = rtrim($this->jumbleFeedNotesBase, '/'); |
||||||
|
|
||||||
|
return $b === '' ? $naddrOrNeventOrNoteBech32 : $b.'/'.$naddrOrNeventOrNoteBech32; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Twig; |
||||||
|
|
||||||
|
use App\Entity\Article; |
||||||
|
use App\Service\NostrPathHelper; |
||||||
|
use Twig\Extension\AbstractExtension; |
||||||
|
use Twig\TwigFunction; |
||||||
|
|
||||||
|
final class NostrPathExtension extends AbstractExtension |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
private readonly NostrPathHelper $nostrPathHelper, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public function getFunctions(): array |
||||||
|
{ |
||||||
|
return [ |
||||||
|
new TwigFunction('npub_from_hex', $this->npubFromHex(...)), |
||||||
|
new TwigFunction('article_path', $this->articlePath(...)), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public function npubFromHex(string $pubkeyHex): string |
||||||
|
{ |
||||||
|
if (64 !== \strlen($pubkeyHex) || !ctype_xdigit($pubkeyHex)) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
return $this->nostrPathHelper->npubFromPubkeyHex($pubkeyHex); |
||||||
|
} |
||||||
|
|
||||||
|
public function articlePath(Article $article): string |
||||||
|
{ |
||||||
|
return $this->nostrPathHelper->articlePath($article); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,37 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace App\Twig; |
||||||
|
|
||||||
|
use App\Dto\NostrShareMenuContext; |
||||||
|
use App\Service\NostrShareMenuBuilder; |
||||||
|
use Symfony\Component\HttpFoundation\RequestStack; |
||||||
|
use Twig\Extension\AbstractExtension; |
||||||
|
use Twig\TwigFunction; |
||||||
|
|
||||||
|
final class NostrShareMenuExtension extends AbstractExtension |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
private readonly NostrShareMenuBuilder $builder, |
||||||
|
private readonly RequestStack $requestStack, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public function getFunctions(): array |
||||||
|
{ |
||||||
|
return [ |
||||||
|
new TwigFunction('nostr_share_menu', [$this, 'getOrBuildContext']), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public function getOrBuildContext(): ?NostrShareMenuContext |
||||||
|
{ |
||||||
|
$request = $this->requestStack->getCurrentRequest(); |
||||||
|
if ($request === null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return $this->builder->buildForRequest($request); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
{% set share = nostr_share_menu() %} |
||||||
|
{% if share is not null %} |
||||||
|
<details class="nostr-share-menu"> |
||||||
|
<summary class="nostr-share-menu__trigger btn btn-secondary btn-sm" title="Nostr options" aria-label="Nostr options">⋯</summary> |
||||||
|
<ul class="nostr-share-menu__list" role="menu"> |
||||||
|
{% if share.npub is not null and share.npub is not same as('') %} |
||||||
|
<li class="nostr-share-menu__item" role="none" |
||||||
|
data-controller="copy-text" |
||||||
|
data-copy-text-text-value="{{ share.npub|e('html_attr') }}"> |
||||||
|
<button type="button" class="nostr-share-menu__action" data-action="click->copy-text#copy" data-copy-text-target="button" role="menuitem">Copy npub</button> |
||||||
|
</li> |
||||||
|
{% endif %} |
||||||
|
{% if share.naddrBech32 is not null and share.naddrBech32 is not same as('') %} |
||||||
|
<li class="nostr-share-menu__item" role="none" |
||||||
|
data-controller="copy-text" |
||||||
|
data-copy-text-text-value="{{ share.naddrBech32|e('html_attr') }}"> |
||||||
|
<button type="button" class="nostr-share-menu__action" data-action="click->copy-text#copy" data-copy-text-target="button" role="menuitem">Copy naddr</button> |
||||||
|
</li> |
||||||
|
{% elseif share.neventBech32 is not null and share.neventBech32 is not same as('') %} |
||||||
|
<li class="nostr-share-menu__item" role="none" |
||||||
|
data-controller="copy-text" |
||||||
|
data-copy-text-text-value="{{ share.neventBech32|e('html_attr') }}"> |
||||||
|
<button type="button" class="nostr-share-menu__action" data-action="click->copy-text#copy" data-copy-text-target="button" role="menuitem">Copy nevent</button> |
||||||
|
</li> |
||||||
|
{% endif %} |
||||||
|
<li class="nostr-share-menu__item" role="none"> |
||||||
|
<a class="nostr-share-menu__action" role="menuitem" href="{{ share.jumbleHref|e('html_attr') }}" target="_blank" rel="nofollow noopener noreferrer">View on Jumble</a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</details> |
||||||
|
{% endif %} |
||||||
Loading…
Reference in new issue