Browse Source

Magazine from RSS, continued

imwald
Nuša Pukšič 3 months ago
parent
commit
18d3053d40
  1. 82
      assets/controllers/nzine_magazine_publish_controller.js
  2. 15
      src/Command/RssFetchCommand.php
  3. 34
      src/Controller/DefaultController.php
  4. 181
      src/Controller/MagazineWizardController.php
  5. 10
      src/Controller/NzineController.php
  6. 310
      src/Service/NzineCategoryIndexService.php
  7. 12
      src/Twig/Components/Organisms/ZineList.php
  8. 25
      templates/magazine/magazine_review.html.twig
  9. 2
      templates/nzine/list.html.twig

82
assets/controllers/nzine_magazine_publish_controller.js

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['status', 'publishButton'];
static values = {
categoryEvents: String,
magazineEvent: String,
publishUrl: String,
nzineSlug: String,
csrfToken: String
};
async publish(event) {
event.preventDefault();
if (!this.publishUrlValue || !this.csrfTokenValue || !this.nzineSlugValue) {
this.showError('Missing configuration');
return;
}
this.publishButtonTarget.disabled = true;
try {
const categoryEvents = JSON.parse(this.categoryEventsValue || '[]');
const magazineEvent = JSON.parse(this.magazineEventValue || '{}');
this.showStatus('Publishing magazine and categories...');
const response = await fetch(this.publishUrlValue, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': this.csrfTokenValue,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
nzineSlug: this.nzineSlugValue,
categoryEvents: categoryEvents,
magazineEvent: magazineEvent
})
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || `HTTP ${response.status}`);
}
const result = await response.json();
this.showSuccess(result.message || 'Magazine published successfully!');
// Redirect to home or magazine page after a short delay
setTimeout(() => {
window.location.href = '/';
}, 2000);
} catch (e) {
console.error(e);
this.showError(e.message || 'Publish failed');
} finally {
this.publishButtonTarget.disabled = false;
}
}
showStatus(message) {
if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `<div class="alert alert-info">${message}</div>`;
}
}
showSuccess(message) {
if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `<div class="alert alert-success">${message}</div>`;
}
}
showError(message) {
if (this.hasStatusTarget) {
this.statusTarget.innerHTML = `<div class="alert alert-danger">${message}</div>`;
}
}
}

15
src/Command/RssFetchCommand.php

@ -461,9 +461,17 @@ class RssFetchCommand extends Command @@ -461,9 +461,17 @@ class RssFetchCommand extends Command
try {
$this->categoryIndexService->addArticleToCategoryIndex(
$categoryIndices[$categorySlug],
$articleCoordinate
$articleCoordinate,
$nzine
);
// Flush to ensure the category index is saved to the database
$this->entityManager->flush();
$this->logger->debug('Added article to category index', [
'article_slug' => $article->getSlug(),
'category_slug' => $categorySlug,
'coordinate' => $articleCoordinate,
]);
} catch (\Exception $e) {
$this->logger->warning('Failed to add article to category index', [
'article_slug' => $article->getSlug(),
@ -471,6 +479,11 @@ class RssFetchCommand extends Command @@ -471,6 +479,11 @@ class RssFetchCommand extends Command
'error' => $e->getMessage(),
]);
}
} else {
$this->logger->warning('Category index not found for matched category', [
'category_slug' => $categorySlug,
'available_indices' => array_keys($categoryIndices),
]);
}
}

34
src/Controller/DefaultController.php

@ -60,10 +60,26 @@ class DefaultController extends AbstractController @@ -60,10 +60,26 @@ class DefaultController extends AbstractController
* @throws InvalidArgumentException
*/
#[Route('/mag/{mag}', name: 'magazine-index')]
public function magIndex(string $mag, RedisCacheService $redisCache) : Response
public function magIndex(string $mag, EntityManagerInterface $entityManager) : Response
{
// redis cache lookup of magazine index by slug
$magazine = $redisCache->getMagazineIndex($mag);
// Get latest magazine index by slug from database
$nzines = $entityManager->getRepository(Event::class)->findBy(['kind' => KindsEnum::PUBLICATION_INDEX]);
// Filter by slug
$nzines = array_filter($nzines, function ($index) use ($mag) {
return $index->getSlug() === $mag;
});
if (count($nzines) === 0) {
throw $this->createNotFoundException('Magazine not found');
}
// Sort by createdAt, keep newest
usort($nzines, function ($a, $b) {
return $b->getCreatedAt() <=> $a->getCreatedAt();
});
$magazine = array_shift($nzines);
return $this->render('magazine/magazine-front.html.twig', [
'magazine' => $magazine,
@ -197,9 +213,15 @@ class DefaultController extends AbstractController @@ -197,9 +213,15 @@ class DefaultController extends AbstractController
}
}
// Create a simple stdClass object with the tags for template compatibility
$catIndex = new \stdClass();
$catIndex->tags = $tags;
// Create a proper Event object for template compatibility
$catIndex = new \swentel\nostr\Event\Event();
$catIndex->setId($eventData['id']);
$catIndex->setPublicKey($eventData['pubkey']);
$catIndex->setCreatedAt($eventData['created_at']);
$catIndex->setKind($eventData['kind']);
$catIndex->setTags($tags);
$catIndex->setContent($eventData['content']);
$catIndex->setSignature($eventData['sig']);
return $this->render('pages/category.html.twig', [
'mag' => $mag,

181
src/Controller/MagazineWizardController.php

@ -6,13 +6,17 @@ namespace App\Controller; @@ -6,13 +6,17 @@ namespace App\Controller;
use App\Dto\CategoryDraft;
use App\Dto\MagazineDraft;
use App\Entity\Nzine;
use App\Enum\KindsEnum;
use App\Form\CategoryArticlesType;
use App\Form\MagazineSetupType;
use App\Repository\NzineRepository;
use App\Service\EncryptionService;
use App\Service\RedisCacheService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Cache\CacheItemPoolInterface;
use swentel\nostr\Event\Event;
use swentel\nostr\Sign\Sign;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
@ -89,13 +93,23 @@ class MagazineWizardController extends AbstractController @@ -89,13 +93,23 @@ class MagazineWizardController extends AbstractController
}
#[Route('/magazine/wizard/review', name: 'mag_wizard_review')]
public function review(Request $request): Response
public function review(Request $request, NzineRepository $nzineRepository): Response
{
$draft = $this->getDraft($request);
if (!$draft) {
return $this->redirectToRoute('mag_wizard_setup');
}
// Check if this slug belongs to an NZine (which has a bot)
$nzine = null;
$isNzineEdit = false;
if ($draft->slug) {
$nzine = $nzineRepository->findOneBy(['slug' => $draft->slug]);
if ($nzine && $nzine->getNzineBot()) {
$isNzineEdit = true;
}
}
// Build event skeletons (without pubkey/sig/id); created_at client can adjust
$categoryEvents = [];
foreach ($draft->categories as $cat) {
@ -117,15 +131,25 @@ class MagazineWizardController extends AbstractController @@ -117,15 +131,25 @@ class MagazineWizardController extends AbstractController
}
// Determine current user's pubkey (hex) from their npub (user identifier)
// For NZine edits, use the NZine's npub instead
$pubkeyHex = null;
$user = $this->getUser();
if ($user && method_exists($user, 'getUserIdentifier')) {
if ($isNzineEdit && $nzine) {
try {
$key = new Key();
$pubkeyHex = $key->convertToHex($user->getUserIdentifier());
$pubkeyHex = $key->convertToHex($nzine->getNpub());
} catch (\Throwable $e) {
$pubkeyHex = null;
}
} else {
$user = $this->getUser();
if ($user && method_exists($user, 'getUserIdentifier')) {
try {
$key = new Key();
$pubkeyHex = $key->convertToHex($user->getUserIdentifier());
} catch (\Throwable $e) {
$pubkeyHex = null;
}
}
}
$magTags = [];
@ -158,6 +182,8 @@ class MagazineWizardController extends AbstractController @@ -158,6 +182,8 @@ class MagazineWizardController extends AbstractController
'categoryEventsJson' => json_encode($categoryEvents, JSON_UNESCAPED_SLASHES),
'magazineEventJson' => json_encode($magazineEvent, JSON_UNESCAPED_SLASHES),
'csrfToken' => $this->container->get('security.csrf.token_manager')->getToken('nostr_publish')->getValue(),
'isNzineEdit' => $isNzineEdit,
'nzineSlug' => $isNzineEdit ? $draft->slug : null,
]);
}
@ -250,6 +276,153 @@ class MagazineWizardController extends AbstractController @@ -250,6 +276,153 @@ class MagazineWizardController extends AbstractController
return new JsonResponse(['ok' => true]);
}
#[Route('/api/nzine-index/publish', name: 'api-nzine-index-publish', methods: ['POST'])]
public function publishNzineIndexEvent(
Request $request,
CsrfTokenManagerInterface $csrfTokenManager,
NzineRepository $nzineRepository,
EncryptionService $encryptionService,
CacheItemPoolInterface $redisCache,
RedisClient $redis,
EntityManagerInterface $entityManager
): JsonResponse {
// Verify CSRF token
$csrfToken = $request->headers->get('X-CSRF-TOKEN');
if (!$csrfTokenManager->isTokenValid(new CsrfToken('nostr_publish', $csrfToken))) {
return new JsonResponse(['error' => 'Invalid CSRF token'], 403);
}
$data = json_decode($request->getContent(), true);
if (!$data || !isset($data['nzineSlug']) || !isset($data['categoryEvents']) || !isset($data['magazineEvent'])) {
return new JsonResponse(['error' => 'Invalid request'], 400);
}
$nzineSlug = $data['nzineSlug'];
$categorySkeletons = $data['categoryEvents'];
$magazineSkeleton = $data['magazineEvent'];
// Load the NZine entity
$nzine = $nzineRepository->findOneBy(['slug' => $nzineSlug]);
if (!$nzine || !$nzine->getNzineBot()) {
return new JsonResponse(['error' => 'NZine not found or no bot configured'], 404);
}
// Get the bot's nsec for signing
$bot = $nzine->getNzineBot();
$bot->setEncryptionService($encryptionService);
$nsec = $bot->getNsec();
if (!$nsec) {
return new JsonResponse(['error' => 'Bot credentials not available'], 500);
}
$key = new Key();
$pubkeyHex = $key->getPublicKey($nsec);
$signer = new Sign();
$categoryCoordinates = [];
try {
// 1) Sign and publish each category event
foreach ($categorySkeletons as $catSkeleton) {
$catEvent = new Event();
$catEvent->setKind($catSkeleton['kind'] ?? 30040);
$catEvent->setCreatedAt($catSkeleton['created_at'] ?? time());
$catEvent->setTags($catSkeleton['tags'] ?? []);
$catEvent->setContent($catSkeleton['content'] ?? '');
// Sign with bot's nsec
$signer->signEvent($catEvent, $nsec);
// Extract slug from d tag
$slug = null;
foreach ($catEvent->getTags() as $tag) {
if (($tag[0] ?? null) === 'd' && isset($tag[1])) {
$slug = $tag[1];
break;
}
}
if (!$slug) {
return new JsonResponse(['error' => 'Category missing d tag'], 400);
}
// Save to Redis
$cacheKey = 'magazine-' . $slug;
$item = $redisCache->getItem($cacheKey);
$item->set($catEvent);
$redisCache->save($item);
// Save to database
$eventEntity = new \App\Entity\Event();
$eventEntity->setId($catEvent->getId());
$eventEntity->setPubkey($catEvent->getPublicKey());
$eventEntity->setCreatedAt($catEvent->getCreatedAt());
$eventEntity->setKind($catEvent->getKind());
$eventEntity->setTags($catEvent->getTags());
$eventEntity->setContent($catEvent->getContent());
$eventEntity->setSig($catEvent->getSignature());
$entityManager->persist($eventEntity);
// Build coordinate
$categoryCoordinates[] = sprintf('30040:%s:%s', $pubkeyHex, $slug);
}
// 2) Build and sign the magazine event with category references
$magEvent = new Event();
$magEvent->setKind($magazineSkeleton['kind'] ?? 30040);
$magEvent->setCreatedAt($magazineSkeleton['created_at'] ?? time());
// Remove any existing 'a' tags and add the new category coordinates
$magTags = array_filter($magazineSkeleton['tags'] ?? [], fn($t) => ($t[0] ?? null) !== 'a');
foreach ($categoryCoordinates as $coord) {
$magTags[] = ['a', $coord];
}
$magEvent->setTags($magTags);
$magEvent->setContent($magazineSkeleton['content'] ?? '');
// Sign with bot's nsec
$signer->signEvent($magEvent, $nsec);
// Extract magazine slug
$magSlug = null;
foreach ($magEvent->getTags() as $tag) {
if (($tag[0] ?? null) === 'd' && isset($tag[1])) {
$magSlug = $tag[1];
break;
}
}
if (!$magSlug) {
return new JsonResponse(['error' => 'Magazine missing d tag'], 400);
}
// Save magazine to Redis
$cacheKey = 'magazine-' . $magSlug;
$item = $redisCache->getItem($cacheKey);
$item->set($magEvent);
$redisCache->save($item);
// Save magazine to database
$magEventEntity = new \App\Entity\Event();
$magEventEntity->setId($magEvent->getId());
$magEventEntity->setPubkey($magEvent->getPublicKey());
$magEventEntity->setCreatedAt($magEvent->getCreatedAt());
$magEventEntity->setKind($magEvent->getKind());
$magEventEntity->setTags($magEvent->getTags());
$magEventEntity->setContent($magEvent->getContent());
$magEventEntity->setSig($magEvent->getSignature());
$entityManager->persist($magEventEntity);
// Record slug in Redis set for admin listing
$redis->sAdd('magazine_slugs', $magSlug);
$entityManager->flush();
return new JsonResponse(['ok' => true, 'message' => 'NZine magazine updated successfully']);
} catch (\Throwable $e) {
return new JsonResponse(['error' => $e->getMessage()], 500);
}
}
#[Route('/magazine/wizard/cancel', name: 'mag_wizard_cancel', methods: ['GET'])]
public function cancel(Request $request): Response
{

10
src/Controller/NzineController.php

@ -302,20 +302,20 @@ class NzineController extends AbstractController @@ -302,20 +302,20 @@ class NzineController extends AbstractController
}
#[Route('/nzine/v/{npub}', name: 'nzine_view')]
public function nzineView($npub, EntityManagerInterface $entityManager): Response
#[Route('/nzine/v/{pubkey}', name: 'nzine_view')]
public function nzineView($pubkey, EntityManagerInterface $entityManager): Response
{
$nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $npub]);
$nzine = $entityManager->getRepository(Nzine::class)->findOneBy(['npub' => $pubkey]);
if (!$nzine) {
throw $this->createNotFoundException('N-Zine not found');
}
// Find all index events for this nzine
$indices = $entityManager->getRepository(EventEntity::class)->findBy(['pubkey' => $npub, 'kind' => KindsEnum::PUBLICATION_INDEX]);
$indices = $entityManager->getRepository(EventEntity::class)->findBy(['pubkey' => $pubkey, 'kind' => KindsEnum::PUBLICATION_INDEX]);
$mainIndexCandidates = array_filter($indices, function ($index) use ($nzine) {
return $index->getSlug() == $nzine->getSlug();
});
dump($indices);die();
dump($indices, $mainIndexCandidates);die();
$mainIndex = array_pop($mainIndexCandidates);

310
src/Service/NzineCategoryIndexService.php

@ -0,0 +1,310 @@ @@ -0,0 +1,310 @@
<?php
namespace App\Service;
use App\Entity\Event as EventEntity;
use App\Entity\Nzine;
use App\Enum\KindsEnum;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use swentel\nostr\Event\Event;
use swentel\nostr\Sign\Sign;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\String\Slugger\AsciiSlugger;
/**
* Service for managing category index events for nzines
*/
class NzineCategoryIndexService
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly EncryptionService $encryptionService,
private readonly LoggerInterface $logger
) {
}
/**
* Ensure category index events exist for all categories in a nzine
* Creates missing category index events and returns them
*
* @param Nzine $nzine The nzine entity
* @return array Map of category slug => EventEntity
* @throws \JsonException
*/
public function ensureCategoryIndices(Nzine $nzine): array
{
$categories = $nzine->getMainCategories();
if (empty($categories)) {
return [];
}
$bot = $nzine->getNzineBot();
if (!$bot) {
$this->logger->warning('Cannot create category indices: nzine bot not found', [
'nzine_id' => $nzine->getId(),
]);
return [];
}
$bot->setEncryptionService($this->encryptionService);
$privateKey = $bot->getNsec();
if (!$privateKey) {
$this->logger->warning('Cannot create category indices: bot private key not found', [
'nzine_id' => $nzine->getId(),
]);
return [];
}
$slugger = new AsciiSlugger();
$categoryIndices = [];
// Load all existing category indices for this nzine at once
$existingIndices = $this->entityManager->getRepository(EventEntity::class)
->findBy([
'pubkey' => $nzine->getNpub(),
'kind' => KindsEnum::PUBLICATION_INDEX->value,
]);
// Index existing events by their d-tag (slug)
$existingBySlug = [];
foreach ($existingIndices as $existingIndex) {
$slug = $this->extractSlugFromTags($existingIndex->getTags());
if ($slug) {
$existingBySlug[$slug] = $existingIndex;
}
}
foreach ($categories as $category) {
if (empty($category['title'])) {
continue;
}
$title = $category['title'];
$slug = !empty($category['slug'])
? $category['slug']
: $slugger->slug($title)->lower()->toString();
// Check if category index already exists
if (isset($existingBySlug[$slug])) {
$this->logger->debug('Using existing category index', [
'category_slug' => $slug,
'title' => $title,
]);
continue;
}
// Create new category index event
$event = new Event();
$event->setKind(KindsEnum::PUBLICATION_INDEX->value);
$event->addTag(['d', $slug]);
$event->addTag(['title', $title]);
$event->addTag(['auto-update', 'yes']);
$event->addTag(['type', 'magazine']);
// Add tags for RSS matching
if (isset($category['tags']) && is_array($category['tags'])) {
foreach ($category['tags'] as $tag) {
$event->addTag(['t', $tag]);
}
}
$event->setPublicKey($nzine->getNpub());
// Sign the event
$signer = new Sign();
$signer->signEvent($event, $privateKey);
// Convert to EventEntity and save
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
$eventEntity = $serializer->deserialize($event->toJson(), EventEntity::class, 'json');
$this->entityManager->persist($eventEntity);
$categoryIndices[$slug] = $eventEntity;
$this->logger->info('Created category index event', [
'nzine_id' => $nzine->getId(),
'category_title' => $title,
'category_slug' => $slug,
]);
}
$this->entityManager->flush();
$this->logger->info('Category indices ready', [
'nzine_id' => $nzine->getId(),
'total_categories' => count($categories),
'indexed_by_slug' => array_keys($categoryIndices),
]);
return $categoryIndices;
}
/**
* Extract the slug (d-tag value) from event tags
*/
private function extractSlugFromTags(array $tags): ?string
{
foreach ($tags as $tag) {
if (is_array($tag) && $tag[0] === 'd' && isset($tag[1])) {
return $tag[1];
}
}
return null;
}
/**
* Add an article to a category index
*
* @param EventEntity $categoryIndex The category index event
* @param string $articleCoordinate The article coordinate (kind:pubkey:slug)
* @param Nzine $nzine The nzine entity (needed for signing)
*/
public function addArticleToCategoryIndex(EventEntity $categoryIndex, string $articleCoordinate, Nzine $nzine): void
{
// Check if article already exists in the index
$tags = $categoryIndex->getTags();
foreach ($tags as $tag) {
if ($tag[0] === 'a' && isset($tag[1]) && $tag[1] === $articleCoordinate) {
// Article already in index
return;
}
}
// Get the bot and private key for signing
$bot = $nzine->getNzineBot();
if (!$bot) {
throw new \RuntimeException('Cannot sign category index: nzine bot not found');
}
$bot->setEncryptionService($this->encryptionService);
$privateKey = $bot->getNsec();
if (!$privateKey) {
throw new \RuntimeException('Cannot sign category index: bot private key not found');
}
// Add article coordinate to tags
$tags[] = ['a', $articleCoordinate];
// Create a new Event object with updated tags
$event = new Event();
$event->setKind($categoryIndex->getKind());
$event->setContent($categoryIndex->getContent() ?? '');
$event->setPublicKey($categoryIndex->getPubkey());
// Add all tags including the new article coordinate
foreach ($tags as $tag) {
$event->addTag($tag);
}
// Sign the event with current timestamp
$signer = new Sign();
$signer->signEvent($event, $privateKey);
// Convert to JSON and back to get all properties including sig
$eventJson = $event->toJson();
$eventData = json_decode($eventJson, true);
// Update the EventEntity with new tags, signature, ID, and timestamp
$categoryIndex->setTags($tags);
$categoryIndex->setSig($eventData['sig']);
$categoryIndex->setId($eventData['id']);
$categoryIndex->setEventId($eventData['id']);
$categoryIndex->setCreatedAt($eventData['created_at']);
$this->entityManager->persist($categoryIndex);
$this->logger->debug('Added article to category index and re-signed', [
'category_slug' => $this->extractSlugFromTags($tags),
'article_coordinate' => $articleCoordinate,
'event_id' => $eventData['id'],
]);
}
/**
* Re-sign and save category index events
* Should be called after all articles have been added to ensure valid signatures
*
* @param array $categoryIndices Map of category slug => EventEntity
* @param Nzine $nzine The nzine entity
*/
public function resignCategoryIndices(array $categoryIndices, Nzine $nzine): void
{
if (empty($categoryIndices)) {
return;
}
$bot = $nzine->getNzineBot();
if (!$bot) {
$this->logger->warning('Cannot re-sign category indices: nzine bot not found', [
'nzine_id' => $nzine->getId(),
]);
return;
}
$bot->setEncryptionService($this->encryptionService);
$privateKey = $bot->getNsec();
if (!$privateKey) {
$this->logger->warning('Cannot re-sign category indices: bot private key not found', [
'nzine_id' => $nzine->getId(),
]);
return;
}
$signer = new Sign();
$serializer = new Serializer([new ObjectNormalizer()], [new JsonEncoder()]);
foreach ($categoryIndices as $slug => $categoryIndex) {
try {
// Create a new Event from the existing EventEntity
$event = new Event();
$event->setKind($categoryIndex->getKind());
$event->setContent($categoryIndex->getContent() ?? '');
$event->setPublicKey($categoryIndex->getPubkey());
// Add all tags from the category index
foreach ($categoryIndex->getTags() as $tag) {
$event->addTag($tag);
}
// Sign the event with current timestamp
$signer->signEvent($event, $privateKey);
// Convert to JSON and back to get all properties including sig
$eventJson = $event->toJson();
$eventData = json_decode($eventJson, true);
// Update the EventEntity with new signature and timestamp
$categoryIndex->setSig($eventData['sig']);
$categoryIndex->setId($eventData['id']);
$categoryIndex->setEventId($eventData['id']);
$categoryIndex->setCreatedAt($eventData['created_at']);
$this->entityManager->persist($categoryIndex);
$this->logger->info('Re-signed category index', [
'category_slug' => $slug,
'event_id' => $eventData['id'],
'article_count' => count(array_filter($categoryIndex->getTags(), fn($tag) => $tag[0] === 'a')),
]);
} catch (\Exception $e) {
$this->logger->error('Failed to re-sign category index', [
'category_slug' => $slug,
'error' => $e->getMessage(),
]);
}
}
$this->entityManager->flush();
$this->logger->info('Category indices re-signed', [
'nzine_id' => $nzine->getId(),
'count' => count($categoryIndices),
]);
}
}

12
src/Twig/Components/Organisms/ZineList.php

@ -3,7 +3,6 @@ @@ -3,7 +3,6 @@
namespace App\Twig\Components\Organisms;
use App\Entity\Event;
use App\Entity\Nzine;
use App\Enum\KindsEnum;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
@ -23,7 +22,7 @@ final class ZineList @@ -23,7 +22,7 @@ final class ZineList
$nzines = $this->entityManager->getRepository(Event::class)->findBy(['kind' => KindsEnum::PUBLICATION_INDEX]);
// filter, only keep type === magazine
$this->nzines = array_filter($nzines, function ($index) {
$filtered = array_filter($nzines, function ($index) {
// look for tags
$tags = $index->getTags();
$isMagType = false;
@ -46,6 +45,15 @@ final class ZineList @@ -46,6 +45,15 @@ final class ZineList
return $isMagType && $isTopLevel;
});
// Deduplicate by slug
$uniqueNzines = [];
foreach ($filtered as $nzine) {
$slug = $nzine->getSlug();
if ($slug !== null) {
$uniqueNzines[$slug] = $nzine;
}
}
$this->nzines = array_values($uniqueNzines);
}
}

25
templates/magazine/magazine_review.html.twig

@ -3,9 +3,15 @@ @@ -3,9 +3,15 @@
{% block body %}
<h1>Review & Sign</h1>
{% if isNzineEdit %}
<div class="notice info">
<p>You are editing an NZine. Changes will be signed automatically using the NZine bot's credentials.</p>
</div>
{% else %}
<div class="notice info">
<p>Review the details below. When ready, click Sign & Publish. Your NIP-07 extension will be used to sign events.</p>
</div>
{% endif %}
<section class="mb-4">
<h2>Magazine</h2>
@ -62,6 +68,24 @@ @@ -62,6 +68,24 @@
</details>
</section>
{% if isNzineEdit %}
<div
{{ stimulus_controller('nzine-magazine-publish', {
categoryEvents: categoryEventsJson,
magazineEvent: magazineEventJson,
publishUrl: path('api-nzine-index-publish'),
nzineSlug: nzineSlug,
csrfToken: csrfToken
}) }}
>
<div class="d-flex gap-2">
<a class="btn btn-secondary" href="{{ path('mag_wizard_cancel') }}">Cancel</a>
<button class="btn btn-primary" data-nzine-magazine-publish-target="publishButton" data-action="click->nzine-magazine-publish#publish">Publish</button>
</div>
<div class="mt-3" data-nzine-magazine-publish-target="status"></div>
</div>
{% else %}
<div
{{ stimulus_controller('nostr-index-sign', {
categoryEvents: categoryEventsJson,
@ -82,4 +106,5 @@ @@ -82,4 +106,5 @@
<div class="mt-3" data-nostr-index-sign-target="status"></div>
</div>
{% endif %}
{% endblock %}

2
templates/nzine/list.html.twig

@ -30,7 +30,7 @@ @@ -30,7 +30,7 @@
<div class="d-flex flex-row gap-2">
<a class="btn btn-sm btn-primary" href="{{ path('nzine_edit', { npub: nzine.npub }) }}">Edit</a>
{% if nzine.hasMainIndex %}
<a class="btn btn-sm btn-outline-primary" href="{{ path('nzine_view', { npub: nzine.npub }) }}">View</a>
<a class="btn btn-sm btn-outline-primary" href="{{ path('nzine_view', { pubkey: nzine.npub }) }}">View</a>
{% else %}
<span class="btn btn-sm btn-outline-secondary disabled" title="Publish the NZine first">View</span>
{% endif %}

Loading…
Cancel
Save