Browse Source

Better profile article list

imwald
Nuša Pukšič 4 days ago
parent
commit
a10901c3cc
  1. 111
      src/Controller/AuthorController.php

111
src/Controller/AuthorController.php

@ -30,6 +30,9 @@ use Symfony\Component\Serializer\SerializerInterface;
class AuthorController extends AbstractController class AuthorController extends AbstractController
{ {
public function __construct(
private readonly LoggerInterface $logger
) {}
/** /**
* Reading List Index * Reading List Index
@ -239,57 +242,45 @@ class AuthorController extends AbstractController
$author = $redisCacheService->getMetadata($pubkey); $author = $redisCacheService->getMetadata($pubkey);
// Try to get cached view first // Check if viewer is the author
$cachedArticles = $viewStore->fetchUserArticles($pubkey); $currentUser = $this->getUser();
$fromCache = false; $isOwnProfile = $currentUser && $currentUser->getUserIdentifier() === $npub;
if ($cachedArticles !== null) { // Query fresh Article entities (not cached view data)
// Redis view data already matches template - just extract articles // This ensures we have proper entities for filtering logic
$articles = [];
foreach ($cachedArticles as $baseObject) {
if (isset($baseObject['article'])) {
$articles[] = (object) $baseObject['article'];
}
}
$fromCache = true;
} else {
// Cache miss - query using search service
$articles = $articleSearch->findByPubkey($pubkey, 100, 0); $articles = $articleSearch->findByPubkey($pubkey, 100, 0);
// Build and cache Redis views for next time // Filter and deduplicate articles at the entity level
$articles = $this->filterAndDeduplicateArticles($articles, $isOwnProfile);
// Build view objects for template from filtered entities
$viewData = [];
if (!empty($articles)) { if (!empty($articles)) {
try { try {
$baseObjects = [];
foreach ($articles as $article) { foreach ($articles as $article) {
if ($article instanceof Article) { if ($article instanceof Article) {
$baseObjects[] = $viewFactory->articleBaseObject($article, $author); $baseObject = $viewFactory->articleBaseObject($article, $author);
$normalized = $viewFactory->normalizeBaseObject($baseObject);
// Extract just the article data and convert to object
// This matches what the template expects (same format as old cache code)
if (isset($normalized['article'])) {
$viewData[] = (object) $normalized['article'];
} }
} }
if (!empty($baseObjects)) {
$viewStore->storeUserArticles($pubkey, $baseObjects);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
// Log but don't fail the request $this->logger->error('Failed to build view objects', [
error_log('Failed to cache user articles view: ' . $e->getMessage()); 'error' => $e->getMessage()
} ]);
} }
} }
$fromCache = false;
// Get latest createdAt for dispatching fetch message // Get latest createdAt for dispatching fetch message
if (!empty($articles)) { if (!empty($articles)) {
// Handle both Article entities and cached arrays // Articles are now guaranteed to be Article entities
if (is_array($articles[0])) {
$latest = isset($articles[0]['article']['publishedAt'])
? strtotime($articles[0]['article']['publishedAt'])
: time();
} else if ($articles[0] instanceof Article) {
// Article entity
$latest = $articles[0]->getCreatedAt()->getTimestamp(); $latest = $articles[0]->getCreatedAt()->getTimestamp();
} else {
// Fallback
// Something went wrong upstream, use current time
$latest = time();
}
// Dispatch async message to fetch new articles since latest + 1 // Dispatch async message to fetch new articles since latest + 1
$messageBus->dispatch(new FetchAuthorArticlesMessage($pubkey, $latest + 1)); $messageBus->dispatch(new FetchAuthorArticlesMessage($pubkey, $latest + 1));
} else { } else {
@ -297,17 +288,65 @@ class AuthorController extends AbstractController
$messageBus->dispatch(new FetchAuthorArticlesMessage($pubkey, 0)); $messageBus->dispatch(new FetchAuthorArticlesMessage($pubkey, 0));
} }
return $this->render('profile/author.html.twig', [ return $this->render('profile/author.html.twig', [
'author' => $author, 'author' => $author,
'npub' => $npub, 'npub' => $npub,
'pubkey' => $pubkey, 'pubkey' => $pubkey,
'articles' => $articles, 'articles' => $viewData, // Pass normalized view data to template
'is_author_profile' => true, 'is_author_profile' => true,
'from_cache' => $fromCache, 'from_cache' => $fromCache,
]); ]);
} }
/**
* Filter and deduplicate articles:
* - Hide drafts (kind 30024) unless viewing own profile
* - Show only the latest version per slug
* - Only handles Article entities (not cached arrays)
*/
private function filterAndDeduplicateArticles(array $articles, bool $isOwnProfile): array
{
$slugMap = [];
foreach ($articles as $article) {
// Only handle Article entities - no more mixed format handling
if (!$article instanceof Article) {
continue;
}
$kind = $article->getKind();
$slug = $article->getSlug();
$createdAt = $article->getCreatedAt();
// Skip drafts unless viewing own profile
if (!$isOwnProfile && $kind === KindsEnum::LONGFORM_DRAFT->value) {
continue;
}
// Skip if no slug
if (!$slug) {
continue;
}
// Keep only the latest version per slug
if (!isset($slugMap[$slug]) || $createdAt > $slugMap[$slug]['createdAt']) {
$slugMap[$slug] = [
'article' => $article,
'createdAt' => $createdAt
];
}
}
// Extract just the articles, sorted by creation date (newest first)
$filtered = array_column($slugMap, 'article');
usort($filtered, function($a, $b) {
return $b->getCreatedAt() <=> $a->getCreatedAt(); // Descending order
});
return $filtered;
}
/** /**
* Redirect from /p/{pubkey} to /p/{npub} * Redirect from /p/{pubkey} to /p/{npub}
* @throws Exception * @throws Exception

Loading…
Cancel
Save