Skip to content

Commit

Permalink
Improve search (#1167)
Browse files Browse the repository at this point in the history
  • Loading branch information
BentiGorlich committed Nov 19, 2024
1 parent 310599a commit 7cd555b
Show file tree
Hide file tree
Showing 18 changed files with 329 additions and 85 deletions.
1 change: 1 addition & 0 deletions assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
@import 'components/figure_image';
@import 'components/figure_lightbox';
@import 'components/post';
@import 'components/search';
@import 'components/subject';
@import 'components/login';
@import 'components/modlog';
Expand Down
24 changes: 24 additions & 0 deletions assets/styles/components/_search.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.search-container {
background: var(--kbin-input-bg);
border: var(--kbin-input-border);
border-radius: var(--kbin-rounded-edges-radius) !important;

input.form-control {
border-radius: 0 !important;
border: none;
background: transparent;
margin: 0 .5em;
padding: .5rem .25rem;
}

button {
border-radius: 0 var(--kbin-rounded-edges-radius) var(--kbin-rounded-edges-radius) 0 !important;
border: 0;
padding: 1rem 0.5rem;

&:not(:hover) {
background: var(--kbin-input-bg);
color: var(--kbin-input-text-color) !important;
}
}
}
6 changes: 6 additions & 0 deletions assets/styles/layout/_forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,9 @@ div.input-box {
border-radius: var(--kbin-rounded-edges-radius) !important;
}
}

.form-control {
display: block;
width: 100%;

}
10 changes: 9 additions & 1 deletion assets/styles/layout/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ figure {
code,
.ts-control > [data-value].item,
.image-preview-container {
border-radius: var(--kbin-rounded-edges-radius) !important;
&:not(.ignore-edges) {
border-radius: var(--kbin-rounded-edges-radius) !important;
}
}

.ts-wrapper {
Expand Down Expand Up @@ -361,6 +363,12 @@ figure {
gap: .25rem;
}

@include media-breakpoint-down(lg) {
.flex.mobile {
display: block;
}
}

.flex-wrap {
flex-wrap: wrap;
}
Expand Down
8 changes: 0 additions & 8 deletions assets/styles/layout/_section.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,3 @@
color: var(--kbin-alert-danger-text-color);
}
}

.page-search {
.section--top {
button {
padding: 1rem 1.5rem;
}
}
}
31 changes: 30 additions & 1 deletion src/Controller/Api/Search/SearchRetrieveApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,27 @@ class SearchRetrieveApi extends BaseApi
required: true,
schema: new OA\Schema(type: 'string')
)]
#[OA\Parameter(
name: 'authorId',
description: 'User id of the author',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer')
)]
#[OA\Parameter(
name: 'magazineId',
description: 'Id of the magazine',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer')
)]
#[OA\Parameter(
name: 'type',
description: 'The type of content',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string', enum: ['', 'entry', 'post'])
)]
#[OA\Tag(name: 'search')]
public function __invoke(
SearchManager $manager,
Expand All @@ -122,8 +143,16 @@ public function __invoke(

$page = $this->getPageNb($request);
$perPage = self::constrainPerPage($request->get('perPage', SearchRepository::PER_PAGE));
$authorIdRaw = $request->get('authorId');
$authorId = null === $authorIdRaw ? null : \intval($authorIdRaw);
$magazineIdRaw = $request->get('magazineId');
$magazineId = null === $magazineIdRaw ? null : \intval($magazineIdRaw);
$type = $request->get('type');
if ('entry' !== $type && 'post' !== $type && null !== $type) {
throw new BadRequestHttpException();
}

$items = $manager->findPaginated($this->getUser(), $q, $page, $perPage);
$items = $manager->findPaginated($this->getUser(), $q, $page, $perPage, authorId: $authorId, magazineId: $magazineId, specificType: $type);
$dtos = [];
foreach ($items->getCurrentPageResults() as $value) {
\assert($value instanceof ContentInterface);
Expand Down
20 changes: 18 additions & 2 deletions src/Controller/Api/User/UserRetrieveApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,18 @@ public function settings(
in: 'query',
schema: new OA\Schema(type: 'string', default: UserRepository::USERS_ALL, enum: UserRepository::USERS_OPTIONS)
)]
#[OA\Parameter(
name: 'q',
description: 'The term to search for',
in: 'query',
schema: new OA\Schema(type: 'string')
)]
#[OA\Parameter(
name: 'withAbout',
description: 'Only include users with a filled in profile',
in: 'query',
schema: new OA\Schema(type: 'boolean')
)]
#[OA\Tag(name: 'user')]
public function collection(
UserRepository $userRepository,
Expand All @@ -286,11 +298,15 @@ public function collection(

$request = $this->request->getCurrentRequest();
$group = $request->get('group', UserRepository::USERS_ALL);
$withAboutRaw = $request->get('withAbout');
$withAbout = null === $withAboutRaw ? false : \boolval($withAboutRaw);

$users = $userRepository->findWithAboutPaginated(
$users = $userRepository->findPaginated(
$this->getPageNb($request),
$withAbout,
$group,
$this->constrainPerPage($request->get('perPage', UserRepository::PER_PAGE))
$this->constrainPerPage($request->get('perPage', UserRepository::PER_PAGE)),
$request->get('q'),
);

$dtos = [];
Expand Down
89 changes: 51 additions & 38 deletions src/Controller/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
namespace App\Controller;

use App\ActivityPub\ActorHandle;
use App\DTO\SearchDto;
use App\Entity\Magazine;
use App\Entity\User;
use App\Form\SearchType;
use App\Message\ActivityPub\Inbox\ActivityMessage;
use App\Service\ActivityPub\ApHttpClient;
use App\Service\ActivityPubManager;
Expand All @@ -33,52 +35,63 @@ public function __construct(

public function __invoke(Request $request): Response
{
$query = $request->query->get('q') ? trim($request->query->get('q')) : null;

if (!$query) {
return $this->render(
'search/front.html.twig',
[
'objects' => [],
'results' => [],
'q' => '',
]
);
}

$this->logger->debug('searching for {query}', ['query' => $query]);

$objects = [];
$dto = new SearchDto();
$form = $this->createForm(SearchType::class, $dto, ['csrf_protection' => false]);
try {
$form = $form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var SearchDto $dto */
$dto = $form->getData();
$query = $dto->q;
$this->logger->debug('searching for {query}', ['query' => $query]);

$objects = [];

// looking up handles (users and mags)
if (str_contains($query, '@') && $this->federatedSearchAllowed()) {
if ($handle = ActorHandle::parse($query)) {
$this->logger->debug('searching for a matched webfinger {query}', ['query' => $query]);
$objects = array_merge($objects, $this->lookupHandle($handle));
} else {
$this->logger->debug("query doesn't look like a valid handle...", ['query' => $query]);
}
}

// looking up handles (users and mags)
if (str_contains($query, '@') && $this->federatedSearchAllowed()) {
if ($handle = ActorHandle::parse($query)) {
$this->logger->debug('searching for a matched webfinger {query}', ['query' => $query]);
$objects = array_merge($objects, $this->lookupHandle($handle));
} else {
$this->logger->debug("query doesn't look like a valid handle...", ['query' => $query]);
}
}
// looking up object by AP id (i.e. urls)
if (false !== filter_var($query, FILTER_VALIDATE_URL)) {
$objects = $this->manager->findByApId($query);
if (!$objects) {
$body = $this->apHttpClient->getActivityObject($query, false);
$this->bus->dispatch(new ActivityMessage($body));
}
}

// looking up object by AP id (i.e. urls)
if (false !== filter_var($query, FILTER_VALIDATE_URL)) {
$objects = $this->manager->findByApId($query);
if (!$objects) {
$body = $this->apHttpClient->getActivityObject($query, false);
$this->bus->dispatch(new ActivityMessage($body));
$user = $this->getUser();
$res = $this->manager->findPaginated($user, $query, $this->getPageNb($request), authorId: $dto->user?->getId(), magazineId: $dto->magazine?->getId(), specificType: $dto->type);

$this->logger->debug('results: {num}', ['num' => $res->count()]);

return $this->render(
'search/front.html.twig',
[
'objects' => $objects,
'results' => $this->overviewManager->buildList($res),
'pagination' => $res,
'form' => $form->createView(),
'q' => $query,
]
);
}
} catch (\Exception $e) {
$this->logger->error($e);
}

$user = $this->getUser();
$res = $this->manager->findPaginated($user, $query, $this->getPageNb($request));

return $this->render(
'search/front.html.twig',
[
'objects' => $objects,
'results' => $this->overviewManager->buildList($res),
'pagination' => $res,
'q' => $request->query->get('q'),
'objects' => [],
'results' => [],
'form' => $form->createView(),
]
);
}
Expand Down
8 changes: 7 additions & 1 deletion src/DTO/SearchDto.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@

namespace App\DTO;

use App\Entity\Magazine;
use App\Entity\User;

class SearchDto
{
public string $val;
public string $q;
public ?string $type = null;
public ?User $user = null;
public ?Magazine $magazine = null;
}
36 changes: 36 additions & 0 deletions src/Form/SearchType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace App\Form;

use App\Form\Type\MagazineAutocompleteType;
use App\Form\Type\UserAutocompleteType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class SearchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->setMethod('GET')
->add('q', TextType::class, [
'required' => true,
'attr' => [
'placeholder' => 'type_search_term',
],
])
->add('magazine', MagazineAutocompleteType::class, ['required' => false])
->add('user', UserAutocompleteType::class, ['required' => false])
->add('type', ChoiceType::class, [
'choices' => [
'search_type_all' => null,
'search_type_entry' => 'entry',
'search_type_post' => 'post',
],
]);
}
}
59 changes: 59 additions & 0 deletions src/Form/Type/UserAutocompleteType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace App\Form\Type;

use App\Entity\Contracts\VisibilityInterface;
use App\Entity\User;
use App\Entity\UserBlock;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;

#[AsEntityAutocompleteField]
class UserAutocompleteType extends AbstractType
{
public function __construct(private readonly Security $security)
{
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'class' => User::class,
'choice_label' => 'username',
'placeholder' => 'select_user',
'filter_query' => function (QueryBuilder $qb, string $query) {
if ($currentUser = $this->security->getUser()) {
$qb
->andWhere(
\sprintf(
'entity.id NOT IN (SELECT IDENTITY(ub.blocked) FROM %s ub WHERE ub.blocker = :user)',
UserBlock::class,
)
)
->setParameter('user', $currentUser);
}

if (!$query) {
return;
}

$qb->andWhere('entity.username LIKE :filter')
->andWhere('entity.visibility = :visibility')
->setParameter('filter', '%'.$query.'%')
->setParameter('visibility', VisibilityInterface::VISIBILITY_VISIBLE)
;
},
]);
}

public function getParent(): string
{
return BaseEntityAutocompleteType::class;
}
}
Loading

0 comments on commit 7cd555b

Please sign in to comment.