diff --git a/assets/app.js b/assets/app.js index 086499e..44623cf 100644 --- a/assets/app.js +++ b/assets/app.js @@ -13,6 +13,7 @@ import './styles/button.css'; import './styles/card.css'; import './styles/article.css'; import './styles/form.css'; +import './styles/notice.css'; import './styles/spinner.css'; import './styles/a2hs.css'; diff --git a/assets/styles/layout.css b/assets/styles/layout.css index 79d25de..b332058 100644 --- a/assets/styles/layout.css +++ b/assets/styles/layout.css @@ -22,7 +22,7 @@ } nav { - width: auto; + width: 21vw; min-width: 150px; flex-shrink: 0; padding: 1em; @@ -104,7 +104,14 @@ main { .user-menu { position: fixed; - top: 180px; + top: 150px; + width: 21vw; + min-width: 150px; +} + +.user-nav { + padding: 10px; + margin: 10px 0; } /* Right sidebar */ diff --git a/assets/styles/notice.css b/assets/styles/notice.css new file mode 100644 index 0000000..d8d908c --- /dev/null +++ b/assets/styles/notice.css @@ -0,0 +1,15 @@ +.notice { + padding: 10px; /* Padding around the content */ + border-radius: 5px; /* Rounded corners */ + margin: 10px 0; /* Margin above and below the notice */ +} + +.notice p { + margin: 0; /* Remove default paragraph margin */ +} + +.notice.info { + 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 */ +} diff --git a/config/packages/fos_elastica.yaml b/config/packages/fos_elastica.yaml index 016e43e..b73d506 100644 --- a/config/packages/fos_elastica.yaml +++ b/config/packages/fos_elastica.yaml @@ -8,12 +8,34 @@ fos_elastica: indexes: # create the index by running php bin/console fos:elastica:populate articles: + settings: + index: + # Increase refresh interval for better write performance + refresh_interval: "5s" + # Optimize indexing + number_of_shards: 1 + number_of_replicas: 0 + analysis: + analyzer: + custom_analyzer: + type: custom + tokenizer: standard + filter: [ lowercase, snowball, asciifolding ] indexable_callback: [ 'App\Util\IndexableArticleChecker', 'isIndexable' ] properties: - createdAt: ~ - title: ~ - summary: ~ - content: ~ + createdAt: + type: keyword + title: + type: text + analyzer: custom_analyzer + content: + type: text + analyzer: custom_analyzer + summary: + type: text + analyzer: custom_analyzer + tags: + type: keyword slug: type: keyword pubkey: diff --git a/src/Entity/User.php b/src/Entity/User.php index ce4d311..5b4c6ce 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -15,8 +15,6 @@ use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Table(name: "app_user")] class User implements UserInterface, EquatableInterface { - private static array $sessionData = []; - #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] @@ -88,22 +86,27 @@ class User implements UserInterface, EquatableInterface public function setMetadata(?object $metadata): void { - self::$sessionData[$this->getNpub()]['metadata'] = $metadata; + $this->metadata = $metadata; } public function getMetadata(): ?object { - return self::$sessionData[$this->getNpub()]['metadata'] ?? null; + return $this->metadata; } public function setRelays(?array $relays): void { - self::$sessionData[$this->getNpub()]['relays'] = $relays; + $this->relays = $relays; } public function getRelays(): ?array { - return self::$sessionData[$this->getNpub()]['relays'] ?? null; + return $this->relays; + } + + public function getName(): ?string + { + return $this->getMetadata()->name ?? $this->getUserIdentifier(); } public function isEqualTo(UserInterface $user): bool diff --git a/src/Security/UserDTOProvider.php b/src/Security/UserDTOProvider.php index c4e5d0c..0e181c4 100644 --- a/src/Security/UserDTOProvider.php +++ b/src/Security/UserDTOProvider.php @@ -48,7 +48,7 @@ readonly class UserDTOProvider implements UserProviderInterface */ public function loadUserByIdentifier(string $identifier): UserInterface { - $this->logger->info('Load user by identifier.'); + $this->logger->info('Load user by identifier.', ['identifier' => $identifier]); // Get or create user $user = $this->entityManager->getRepository(User::class)->findOneBy(['npub' => $identifier]); diff --git a/src/Service/NostrClient.php b/src/Service/NostrClient.php index e03c814..6898fe3 100644 --- a/src/Service/NostrClient.php +++ b/src/Service/NostrClient.php @@ -42,9 +42,7 @@ class NostrClient public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ManagerRegistry $managerRegistry, - private readonly UserEntityRepository $userEntityRepository, private readonly ArticleFactory $articleFactory, - private readonly SerializerInterface $serializer, private readonly TokenStorageInterface $tokenStorage, private readonly LoggerInterface $logger) { @@ -102,49 +100,22 @@ class NostrClient return $reputableAuthorRelays; } - public function getLoginData($npub) - { - $subscription = new Subscription(); - $subscriptionId = $subscription->setId(); - $filter = new Filter(); - $filter->setKinds([KindsEnum::METADATA, KindsEnum::RELAY_LIST]); - $filter->setAuthors([$npub]); - $requestMessage = new RequestMessage($subscriptionId, [$filter]); - $request = new Request($this->defaultRelaySet, $requestMessage); - - $response = $request->send(); - $this->logger->info('Login data.', ['response' => $response]); - - // 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) { - return $filtered; - } - } - - return null; - } - public function getNpubMetadata($npub): \stdClass { $this->logger->info('Getting metadata for npub', ['npub' => $npub]); - // Convert npub to hex - $keys = new Key(); - $pubkey = $keys->convertToHex($npub); + // Npubs are converted to hex for the request down the line $request = $this->createNostrRequest( kinds: [KindsEnum::METADATA], - filters: ['authors' => [$pubkey]], - relaySet: $this->defaultRelaySet + filters: ['authors' => [$npub]] ); $events = $this->processResponse($request->send(), function($received) { + $this->logger->info('Getting metadata for npub', ['item' => $received]); return $received; }); + $this->logger->info('Getting metadata for npub', ['response' => $events]); + if (empty($events)) { $meta = new \stdClass(); $content = new \stdClass(); @@ -320,69 +291,6 @@ class NostrClient } } - /** - * User metadata - * NIP-01 - * @throws \Exception - */ - public function getMetadata(array $npubs): void - { - $subscription = new Subscription(); - $subscriptionId = $subscription->setId(); - $filter = new Filter(); - $filter->setKinds([KindsEnum::METADATA]); - $filter->setAuthors($npubs); - $requestMessage = new RequestMessage($subscriptionId, [$filter]); - // TODO make relays configurable - $relays = new RelaySet(); - $relays->addRelay(new Relay('wss://purplepag.es')); // default metadata aggregator - // $relays->addRelay(new Relay('wss://nos.lol')); // default metadata aggregator - - $request = new Request($relays, $requestMessage); - - $response = $request->send(); - // response is an array of arrays - foreach ($response as $value) { - foreach ($value as $item) { - switch ($item->type) { - case 'EVENT': - $this->saveMetadata($item->event); - break; - case 'AUTH': - throw new UnauthorizedHttpException('', 'Relay requires authentication'); - case 'ERROR': - case 'NOTICE': - throw new \Exception('An error occurred'); - default: - // nothing to do here - } - } - } - } - - /** - * Save user metadata - */ - private function saveMetadata($metadata): void - { - try { - $user = $this->serializer->deserialize($metadata->content, User::class, 'json'); - $user->setNpub($metadata->pubkey); - } catch (\Exception $e) { - $this->logger->error('Deserialization of user data failed.', ['exception' => $e]); - return; - } - - try { - $this->logger->info('Saving user', ['user' => $user]); - $this->userEntityRepository->findOrCreateByUniqueField($user); - $this->entityManager->flush(); - } catch (\Exception $e) { - $this->logger->error($e->getMessage()); - $this->managerRegistry->resetManager(); - } - } - private function saveLongFormContent(mixed $filtered): void { foreach ($filtered as $wrapper) { diff --git a/src/Service/RedisCacheService.php b/src/Service/RedisCacheService.php index d2a6ea8..0f280be 100644 --- a/src/Service/RedisCacheService.php +++ b/src/Service/RedisCacheService.php @@ -25,7 +25,6 @@ readonly class RedisCacheService public function getMetadata(string $npub): \stdClass { $cacheKey = '0_' . $npub; - try { return $this->redisCache->get($cacheKey, function (ItemInterface $item) use ($npub) { $item->expiresAfter(3600); // 1 hour, adjust as needed diff --git a/src/Twig/Components/GetCreditsComponent.php b/src/Twig/Components/GetCreditsComponent.php index ef0eb61..7befebc 100644 --- a/src/Twig/Components/GetCreditsComponent.php +++ b/src/Twig/Components/GetCreditsComponent.php @@ -34,9 +34,7 @@ final class GetCreditsComponent } // Dispatch event to notify parent - $this->emit('creditsAdded', [ - 'credits' => 5, - ]); + $this->emit('creditsAdded'); } } diff --git a/src/Twig/Components/Molecules/UserFromNpub.php b/src/Twig/Components/Molecules/UserFromNpub.php index 56875f7..de04df2 100644 --- a/src/Twig/Components/Molecules/UserFromNpub.php +++ b/src/Twig/Components/Molecules/UserFromNpub.php @@ -17,11 +17,16 @@ final class UserFromNpub { } - public function mount(string $pubkey): void + public function mount(string $ident): void { - $keys = new Key(); - $this->pubkey = $pubkey; - $this->npub = $keys->convertPublicKeyToBech32($pubkey); + // if npub doesn't start with 'npub' then assume it's a hex pubkey + if (!str_starts_with($ident, 'npub')) { + $keys = new Key(); + $this->pubkey = $ident; + $this->npub = $keys->convertPublicKeyToBech32($ident); + } else { + $this->npub = $ident; + } $this->user = $this->redisCacheService->getMetadata($this->npub); } } diff --git a/src/Twig/Components/SearchComponent.php b/src/Twig/Components/SearchComponent.php index abf9610..fe1438b 100644 --- a/src/Twig/Components/SearchComponent.php +++ b/src/Twig/Components/SearchComponent.php @@ -9,10 +9,13 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\UX\LiveComponent\Attribute\AsLiveComponent; use Symfony\UX\LiveComponent\Attribute\LiveAction; -use Symfony\UX\LiveComponent\Attribute\LiveArg; use Symfony\UX\LiveComponent\Attribute\LiveListener; use Symfony\UX\LiveComponent\Attribute\LiveProp; use Symfony\UX\LiveComponent\DefaultActionTrait; +use Symfony\Contracts\Cache\CacheInterface; +use Elastica\Query; +use Elastica\Query\BoolQuery; +use Elastica\Query\MultiMatch; #[AsLiveComponent] final class SearchComponent @@ -31,11 +34,18 @@ final class SearchComponent #[LiveProp] public int $vol = 0; + #[LiveProp(writable: true)] + public int $page = 1; + + #[LiveProp] + public int $resultsPerPage = 12; + public function __construct( private readonly FinderInterface $finder, private readonly CreditsManager $creditsManager, private readonly TokenStorageInterface $tokenStorage, - private readonly LoggerInterface $logger + private readonly LoggerInterface $logger, + private readonly CacheInterface $cache ) { $token = $this->tokenStorage->getToken(); @@ -79,19 +89,79 @@ final class SearchComponent return; } - $this->creditsManager->spendCredits($this->npub, 1, 'search'); - $this->credits--; - - $this->results = array_filter( - $this->finder->find($this->query, 12), - fn($r) => !str_contains($r->getSlug(), '/') - ); + try { + $this->results = []; + $this->creditsManager->spendCredits($this->npub, 1, 'search'); + $this->credits--; + + // Create an optimized query using collapse correctly + $mainQuery = new Query(); + + // Build multi-match query for searching across fields + $multiMatch = new MultiMatch(); + $multiMatch->setQuery($this->query); + $multiMatch->setFields([ + 'title^3', + 'summary^2', + 'content^1.5', + 'topics' + ]); + $multiMatch->setType(MultiMatch::TYPE_MOST_FIELDS); + $multiMatch->setFuzziness('AUTO'); + + $boolQuery = new BoolQuery(); + $boolQuery->addMust($multiMatch); + $boolQuery->addMustNot(new Query\Wildcard('slug', '*/*')); + + // For text fields, we need to use a different approach + // Create a regexp query that matches content with at least 250 chars + // This is a simplification - actually matches content with enough words + $lengthFilter = new Query\QueryString(); + $lengthFilter->setQuery('content:/.{250,}/'); + // $boolQuery->addFilter($lengthFilter); + + $mainQuery->setQuery($boolQuery); + + // Use the collapse field directly in the array format + // This fixes the [collapse] failed to parse field [inner_hits] error + $mainQuery->setParam('collapse', [ + 'field' => 'slug', + 'inner_hits' => [ + 'name' => 'latest_articles', + 'size' => 1 // Show more related articles + ] + ]); + + // Reduce the minimum score threshold to include more results + $mainQuery->setMinScore(0.1); // Lower minimum score + + // Sort by score and createdAt + $mainQuery->setSort([ + '_score' => ['order' => 'desc'], + 'createdAt' => ['order' => 'desc'] + ]); + + // Add pagination + $offset = ($this->page - 1) * $this->resultsPerPage; + $mainQuery->setFrom($offset); + $mainQuery->setSize($this->resultsPerPage); + + // Execute the search + $results = $this->finder->find($mainQuery); + $this->logger->info('Search results count: ' . count($results)); + $this->logger->info('Search results: ', ['results' => $results]); + + $this->results = $results; + } catch (\Exception $e) { + $this->logger->error('Search error: ' . $e->getMessage()); + $this->results = []; + } } #[LiveListener('creditsAdded')] - public function incrementCreditsCount(array $data): void + public function incrementCreditsCount(): void { - $this->credits += $data['credits']; + $this->credits += 5; } } diff --git a/templates/components/Molecules/Card.html.twig b/templates/components/Molecules/Card.html.twig index a8e377e..65d36a7 100644 --- a/templates/components/Molecules/Card.html.twig +++ b/templates/components/Molecules/Card.html.twig @@ -4,7 +4,7 @@ {% if category %} {{ category }} {% else %} -
by
by