Skip to content

Commit 7cd555b

Browse files
committed
Improve search (#1167)
1 parent 310599a commit 7cd555b

18 files changed

+329
-85
lines changed

assets/styles/app.scss

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
@import 'components/figure_image';
3838
@import 'components/figure_lightbox';
3939
@import 'components/post';
40+
@import 'components/search';
4041
@import 'components/subject';
4142
@import 'components/login';
4243
@import 'components/modlog';

assets/styles/components/_search.scss

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
.search-container {
2+
background: var(--kbin-input-bg);
3+
border: var(--kbin-input-border);
4+
border-radius: var(--kbin-rounded-edges-radius) !important;
5+
6+
input.form-control {
7+
border-radius: 0 !important;
8+
border: none;
9+
background: transparent;
10+
margin: 0 .5em;
11+
padding: .5rem .25rem;
12+
}
13+
14+
button {
15+
border-radius: 0 var(--kbin-rounded-edges-radius) var(--kbin-rounded-edges-radius) 0 !important;
16+
border: 0;
17+
padding: 1rem 0.5rem;
18+
19+
&:not(:hover) {
20+
background: var(--kbin-input-bg);
21+
color: var(--kbin-input-text-color) !important;
22+
}
23+
}
24+
}

assets/styles/layout/_forms.scss

+6
Original file line numberDiff line numberDiff line change
@@ -525,3 +525,9 @@ div.input-box {
525525
border-radius: var(--kbin-rounded-edges-radius) !important;
526526
}
527527
}
528+
529+
.form-control {
530+
display: block;
531+
width: 100%;
532+
533+
}

assets/styles/layout/_layout.scss

+9-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ figure {
214214
code,
215215
.ts-control > [data-value].item,
216216
.image-preview-container {
217-
border-radius: var(--kbin-rounded-edges-radius) !important;
217+
&:not(.ignore-edges) {
218+
border-radius: var(--kbin-rounded-edges-radius) !important;
219+
}
218220
}
219221

220222
.ts-wrapper {
@@ -361,6 +363,12 @@ figure {
361363
gap: .25rem;
362364
}
363365

366+
@include media-breakpoint-down(lg) {
367+
.flex.mobile {
368+
display: block;
369+
}
370+
}
371+
364372
.flex-wrap {
365373
flex-wrap: wrap;
366374
}

assets/styles/layout/_section.scss

-8
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,3 @@
6868
color: var(--kbin-alert-danger-text-color);
6969
}
7070
}
71-
72-
.page-search {
73-
.section--top {
74-
button {
75-
padding: 1rem 1.5rem;
76-
}
77-
}
78-
}

src/Controller/Api/Search/SearchRetrieveApi.php

+30-1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,27 @@ class SearchRetrieveApi extends BaseApi
103103
required: true,
104104
schema: new OA\Schema(type: 'string')
105105
)]
106+
#[OA\Parameter(
107+
name: 'authorId',
108+
description: 'User id of the author',
109+
in: 'query',
110+
required: false,
111+
schema: new OA\Schema(type: 'integer')
112+
)]
113+
#[OA\Parameter(
114+
name: 'magazineId',
115+
description: 'Id of the magazine',
116+
in: 'query',
117+
required: false,
118+
schema: new OA\Schema(type: 'integer')
119+
)]
120+
#[OA\Parameter(
121+
name: 'type',
122+
description: 'The type of content',
123+
in: 'query',
124+
required: false,
125+
schema: new OA\Schema(type: 'string', enum: ['', 'entry', 'post'])
126+
)]
106127
#[OA\Tag(name: 'search')]
107128
public function __invoke(
108129
SearchManager $manager,
@@ -122,8 +143,16 @@ public function __invoke(
122143

123144
$page = $this->getPageNb($request);
124145
$perPage = self::constrainPerPage($request->get('perPage', SearchRepository::PER_PAGE));
146+
$authorIdRaw = $request->get('authorId');
147+
$authorId = null === $authorIdRaw ? null : \intval($authorIdRaw);
148+
$magazineIdRaw = $request->get('magazineId');
149+
$magazineId = null === $magazineIdRaw ? null : \intval($magazineIdRaw);
150+
$type = $request->get('type');
151+
if ('entry' !== $type && 'post' !== $type && null !== $type) {
152+
throw new BadRequestHttpException();
153+
}
125154

126-
$items = $manager->findPaginated($this->getUser(), $q, $page, $perPage);
155+
$items = $manager->findPaginated($this->getUser(), $q, $page, $perPage, authorId: $authorId, magazineId: $magazineId, specificType: $type);
127156
$dtos = [];
128157
foreach ($items->getCurrentPageResults() as $value) {
129158
\assert($value instanceof ContentInterface);

src/Controller/Api/User/UserRetrieveApi.php

+18-2
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,18 @@ public function settings(
275275
in: 'query',
276276
schema: new OA\Schema(type: 'string', default: UserRepository::USERS_ALL, enum: UserRepository::USERS_OPTIONS)
277277
)]
278+
#[OA\Parameter(
279+
name: 'q',
280+
description: 'The term to search for',
281+
in: 'query',
282+
schema: new OA\Schema(type: 'string')
283+
)]
284+
#[OA\Parameter(
285+
name: 'withAbout',
286+
description: 'Only include users with a filled in profile',
287+
in: 'query',
288+
schema: new OA\Schema(type: 'boolean')
289+
)]
278290
#[OA\Tag(name: 'user')]
279291
public function collection(
280292
UserRepository $userRepository,
@@ -286,11 +298,15 @@ public function collection(
286298

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

290-
$users = $userRepository->findWithAboutPaginated(
304+
$users = $userRepository->findPaginated(
291305
$this->getPageNb($request),
306+
$withAbout,
292307
$group,
293-
$this->constrainPerPage($request->get('perPage', UserRepository::PER_PAGE))
308+
$this->constrainPerPage($request->get('perPage', UserRepository::PER_PAGE)),
309+
$request->get('q'),
294310
);
295311

296312
$dtos = [];

src/Controller/SearchController.php

+51-38
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
namespace App\Controller;
66

77
use App\ActivityPub\ActorHandle;
8+
use App\DTO\SearchDto;
89
use App\Entity\Magazine;
910
use App\Entity\User;
11+
use App\Form\SearchType;
1012
use App\Message\ActivityPub\Inbox\ActivityMessage;
1113
use App\Service\ActivityPub\ApHttpClient;
1214
use App\Service\ActivityPubManager;
@@ -33,52 +35,63 @@ public function __construct(
3335

3436
public function __invoke(Request $request): Response
3537
{
36-
$query = $request->query->get('q') ? trim($request->query->get('q')) : null;
37-
38-
if (!$query) {
39-
return $this->render(
40-
'search/front.html.twig',
41-
[
42-
'objects' => [],
43-
'results' => [],
44-
'q' => '',
45-
]
46-
);
47-
}
48-
49-
$this->logger->debug('searching for {query}', ['query' => $query]);
50-
51-
$objects = [];
38+
$dto = new SearchDto();
39+
$form = $this->createForm(SearchType::class, $dto, ['csrf_protection' => false]);
40+
try {
41+
$form = $form->handleRequest($request);
42+
if ($form->isSubmitted() && $form->isValid()) {
43+
/** @var SearchDto $dto */
44+
$dto = $form->getData();
45+
$query = $dto->q;
46+
$this->logger->debug('searching for {query}', ['query' => $query]);
47+
48+
$objects = [];
49+
50+
// looking up handles (users and mags)
51+
if (str_contains($query, '@') && $this->federatedSearchAllowed()) {
52+
if ($handle = ActorHandle::parse($query)) {
53+
$this->logger->debug('searching for a matched webfinger {query}', ['query' => $query]);
54+
$objects = array_merge($objects, $this->lookupHandle($handle));
55+
} else {
56+
$this->logger->debug("query doesn't look like a valid handle...", ['query' => $query]);
57+
}
58+
}
5259

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

63-
// looking up object by AP id (i.e. urls)
64-
if (false !== filter_var($query, FILTER_VALIDATE_URL)) {
65-
$objects = $this->manager->findByApId($query);
66-
if (!$objects) {
67-
$body = $this->apHttpClient->getActivityObject($query, false);
68-
$this->bus->dispatch(new ActivityMessage($body));
69+
$user = $this->getUser();
70+
$res = $this->manager->findPaginated($user, $query, $this->getPageNb($request), authorId: $dto->user?->getId(), magazineId: $dto->magazine?->getId(), specificType: $dto->type);
71+
72+
$this->logger->debug('results: {num}', ['num' => $res->count()]);
73+
74+
return $this->render(
75+
'search/front.html.twig',
76+
[
77+
'objects' => $objects,
78+
'results' => $this->overviewManager->buildList($res),
79+
'pagination' => $res,
80+
'form' => $form->createView(),
81+
'q' => $query,
82+
]
83+
);
6984
}
85+
} catch (\Exception $e) {
86+
$this->logger->error($e);
7087
}
7188

72-
$user = $this->getUser();
73-
$res = $this->manager->findPaginated($user, $query, $this->getPageNb($request));
74-
7589
return $this->render(
7690
'search/front.html.twig',
7791
[
78-
'objects' => $objects,
79-
'results' => $this->overviewManager->buildList($res),
80-
'pagination' => $res,
81-
'q' => $request->query->get('q'),
92+
'objects' => [],
93+
'results' => [],
94+
'form' => $form->createView(),
8295
]
8396
);
8497
}

src/DTO/SearchDto.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44

55
namespace App\DTO;
66

7+
use App\Entity\Magazine;
8+
use App\Entity\User;
9+
710
class SearchDto
811
{
9-
public string $val;
12+
public string $q;
13+
public ?string $type = null;
14+
public ?User $user = null;
15+
public ?Magazine $magazine = null;
1016
}

src/Form/SearchType.php

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Form;
6+
7+
use App\Form\Type\MagazineAutocompleteType;
8+
use App\Form\Type\UserAutocompleteType;
9+
use Symfony\Component\Form\AbstractType;
10+
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
11+
use Symfony\Component\Form\Extension\Core\Type\TextType;
12+
use Symfony\Component\Form\FormBuilderInterface;
13+
14+
class SearchType extends AbstractType
15+
{
16+
public function buildForm(FormBuilderInterface $builder, array $options): void
17+
{
18+
$builder
19+
->setMethod('GET')
20+
->add('q', TextType::class, [
21+
'required' => true,
22+
'attr' => [
23+
'placeholder' => 'type_search_term',
24+
],
25+
])
26+
->add('magazine', MagazineAutocompleteType::class, ['required' => false])
27+
->add('user', UserAutocompleteType::class, ['required' => false])
28+
->add('type', ChoiceType::class, [
29+
'choices' => [
30+
'search_type_all' => null,
31+
'search_type_entry' => 'entry',
32+
'search_type_post' => 'post',
33+
],
34+
]);
35+
}
36+
}
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Form\Type;
6+
7+
use App\Entity\Contracts\VisibilityInterface;
8+
use App\Entity\User;
9+
use App\Entity\UserBlock;
10+
use Doctrine\ORM\QueryBuilder;
11+
use Symfony\Bundle\SecurityBundle\Security;
12+
use Symfony\Component\Form\AbstractType;
13+
use Symfony\Component\OptionsResolver\OptionsResolver;
14+
use Symfony\UX\Autocomplete\Form\AsEntityAutocompleteField;
15+
use Symfony\UX\Autocomplete\Form\BaseEntityAutocompleteType;
16+
17+
#[AsEntityAutocompleteField]
18+
class UserAutocompleteType extends AbstractType
19+
{
20+
public function __construct(private readonly Security $security)
21+
{
22+
}
23+
24+
public function configureOptions(OptionsResolver $resolver): void
25+
{
26+
$resolver->setDefaults([
27+
'class' => User::class,
28+
'choice_label' => 'username',
29+
'placeholder' => 'select_user',
30+
'filter_query' => function (QueryBuilder $qb, string $query) {
31+
if ($currentUser = $this->security->getUser()) {
32+
$qb
33+
->andWhere(
34+
\sprintf(
35+
'entity.id NOT IN (SELECT IDENTITY(ub.blocked) FROM %s ub WHERE ub.blocker = :user)',
36+
UserBlock::class,
37+
)
38+
)
39+
->setParameter('user', $currentUser);
40+
}
41+
42+
if (!$query) {
43+
return;
44+
}
45+
46+
$qb->andWhere('entity.username LIKE :filter')
47+
->andWhere('entity.visibility = :visibility')
48+
->setParameter('filter', '%'.$query.'%')
49+
->setParameter('visibility', VisibilityInterface::VISIBILITY_VISIBLE)
50+
;
51+
},
52+
]);
53+
}
54+
55+
public function getParent(): string
56+
{
57+
return BaseEntityAutocompleteType::class;
58+
}
59+
}

0 commit comments

Comments
 (0)