Skip to content

Commit 61793ea

Browse files
feat: Add filters to streams
2 parents 5618895 + 9eed2f0 commit 61793ea

16 files changed

Lines changed: 1443 additions & 113 deletions

File tree

locales/fr_FR/LC_MESSAGES/main.mo

480 Bytes
Binary file not shown.

locales/fr_FR/LC_MESSAGES/main.po

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
msgid ""
22
msgstr ""
33
"Project-Id-Version: Flus\n"
4-
"POT-Creation-Date: 2026-06-26 10:35+0200\n"
5-
"PO-Revision-Date: 2026-06-26 10:35+0200\n"
4+
"POT-Creation-Date: 2026-07-22 14:40+0200\n"
5+
"PO-Revision-Date: 2026-07-22 14:41+0200\n"
66
"Last-Translator: Marien Fressinaud <dev@marienfressinaud.fr>\n"
77
"Language-Team: \n"
88
"Language: fr_FR\n"
@@ -653,7 +653,7 @@ msgstr "aucun lien"
653653
#: views/collections/_collection.html.twig:26
654654
#: views/collections/_collection_follow.html.twig:18
655655
#: views/links/_pagination_count.html.twig:7 views/news/index.html.twig:26
656-
#: views/news/index.html.twig:132 views/streams/show.html.twig:101
656+
#: views/news/index.html.twig:132 views/streams/_timeline.html.twig:44
657657
#, php-format
658658
msgid "%s link"
659659
msgid_plural "%s links"
@@ -856,6 +856,7 @@ msgstr ""
856856
"pour la plupart des flux)."
857857

858858
#: views/collections/filters/edit.html.twig:85
859+
#: views/streams/_filters.html.twig:122
859860
msgid "All"
860861
msgstr "Tout"
861862

@@ -1388,7 +1389,7 @@ msgstr "Se déconnecter"
13881389
msgid "Language"
13891390
msgstr "Langue"
13901391

1391-
#: views/layouts/_header-disconnected.html.twig:60
1392+
#: views/layouts/_header-disconnected.html.twig:59
13921393
#: views/sessions/new.html.twig:104
13931394
msgid "Login"
13941395
msgstr "Se connecter"
@@ -1431,6 +1432,7 @@ msgid "Search links by words, URL or #tag"
14311432
msgstr "Rechercher des liens par mots, URL ou #tag"
14321433

14331434
#: views/layouts/_search.html.twig:15 views/links/searches/show.html.twig:64
1435+
#: views/streams/_filters.html.twig:53
14341436
msgid "Search"
14351437
msgstr "Rechercher"
14361438

@@ -2905,6 +2907,38 @@ msgstr "Mot de passe oublié ?"
29052907
msgid "No account yet?"
29062908
msgstr "Pas encore de compte ?"
29072909

2910+
#: views/streams/_filters.html.twig:12
2911+
msgid "Date"
2912+
msgstr "Date"
2913+
2914+
#: views/streams/_filters.html.twig:33
2915+
msgid "Number of days"
2916+
msgstr "Nombre de jours"
2917+
2918+
#: views/streams/_filters.html.twig:63
2919+
#, php-format
2920+
msgid "Sources (%d)"
2921+
msgstr "Sources (%d)"
2922+
2923+
#: views/streams/_filters.html.twig:82
2924+
#, php-format
2925+
msgid "%s: %d link"
2926+
msgid_plural "%s: %d links"
2927+
msgstr[0] "%s : %d lien"
2928+
msgstr[1] "%s : %d liens"
2929+
2930+
#: views/streams/_filters.html.twig:82
2931+
msgid "(has unread)"
2932+
msgstr "(contient des non lus)"
2933+
2934+
#: views/streams/_filters.html.twig:119
2935+
msgid "Status of the links"
2936+
msgstr "Statut des liens"
2937+
2938+
#: views/streams/_filters.html.twig:123
2939+
msgid "Unread"
2940+
msgstr "Non lus"
2941+
29082942
#: views/streams/_form.html.twig:12
29092943
msgid "What’s the name of the stream?"
29102944
msgstr "Quelle est le nom du fil ?"
@@ -2913,6 +2947,10 @@ msgstr "Quelle est le nom du fil ?"
29132947
msgid "What’s the purpose of the stream?"
29142948
msgstr "Quelle est la raison d’être du fil ?"
29152949

2950+
#: views/streams/_timeline.html.twig:66
2951+
msgid "There is no link matching your filters."
2952+
msgstr "Il n’y a aucun lien qui correspond à vos filtres."
2953+
29162954
#: views/streams/new.html.twig:9
29172955
msgid "Create the stream"
29182956
msgstr "Créer le fil"

src/assets/javascripts/application.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import NewsRefresherController from './controllers/news_refresher_controller.js'
2121
import NotificationController from './controllers/notification_controller.js';
2222
import PopupController from './controllers/popup_controller.js';
2323
import SidenavController from './controllers/sidenav_controller.js';
24+
import StreamController from './controllers/stream_controller.js';
2425
import TextEditorController from './controllers/text_editor_controller.js';
2526
import ZindexInverserController from './controllers/zindex_inverser_controller.js';
2627

@@ -47,6 +48,7 @@ application.register('news-refresher', NewsRefresherController);
4748
application.register('notification', NotificationController);
4849
application.register('popup', PopupController);
4950
application.register('sidenav', SidenavController);
51+
application.register('stream', StreamController);
5052
application.register('text-editor', TextEditorController);
5153
application.register('zindex-inverser', ZindexInverserController);
5254

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Controller } from '@hotwired/stimulus';
2+
3+
export default class extends Controller {
4+
static targets = ['formFilters', 'source', 'sourceInput', 'status', 'statusInput'];
5+
6+
selectSource (event) {
7+
const button = event.currentTarget;
8+
9+
// The sources are toggle buttons: clicking the selected one unselects it.
10+
const willBeSelected = button.getAttribute('aria-pressed') !== 'true';
11+
12+
this.sourceInputTarget.value = willBeSelected ? button.value : '';
13+
this.press(this.sourceTargets, willBeSelected ? button : null);
14+
15+
this.submit();
16+
}
17+
18+
selectStatus (event) {
19+
const button = event.currentTarget;
20+
21+
// Contrary to the sources, a status is always selected: clicking the
22+
// selected one does nothing.
23+
if (button.getAttribute('aria-pressed') === 'true') {
24+
return;
25+
}
26+
27+
this.statusInputTarget.value = button.value;
28+
this.press(this.statusTargets, button);
29+
30+
this.submit();
31+
}
32+
33+
press (buttons, pressedButton) {
34+
buttons.forEach((button) => {
35+
button.setAttribute('aria-pressed', button === pressedButton ? 'true' : 'false');
36+
});
37+
}
38+
39+
submit () {
40+
// requestSubmit() (and not submit()) so Turbo intercepts the
41+
// submission and only renders the timeline frame.
42+
this.formFiltersTarget.requestSubmit();
43+
}
44+
};

src/assets/stylesheets/application.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
@import url('./components/badges.css');
88
@import url('./components/buttons.css');
99
@import url('./components/collections.css');
10+
@import url('./components/filters.css');
1011
@import url('./components/groups.css');
1112
@import url('./components/images.css');
1213
@import url('./components/links.css');
1314
@import url('./components/notepads.css');
1415
@import url('./components/shares.css');
1516
@import url('./components/sources.css');
1617
@import url('./components/threads.css');
18+
@import url('./components/timelines.css');
1719
@import url('./components/topics.css');
1820
@import url('./components/turbo.css');
1921
@import url('./components/usernames.css');
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
.filters {
2+
background-color: var(--color-background);
3+
border: 1px solid var(--color-grey-line);
4+
border-radius: var(--border-radius);
5+
}
6+
7+
.filters__section {
8+
padding: var(--space-small);
9+
}
10+
11+
.filters__section + .filters__section {
12+
border-top: 1px solid var(--color-grey-line);
13+
}
14+
15+
.filters__option {
16+
position: relative;
17+
}
18+
19+
.filters__option button[aria-pressed="true"] {
20+
--button-color: var(--color-accent-text-contrast);
21+
--button-background-color: var(--color-accent-bg-active);
22+
--button-border-color: var(--color-accent-border);
23+
}
24+
25+
.filters__option .source--button[aria-pressed="true"] {
26+
background-color: var(--color-accent-bg-subtle);
27+
border-color: var(--color-accent-border);
28+
}
29+
30+
.filters__option .filters__unselect {
31+
display: none;
32+
}
33+
34+
.filters__option [aria-pressed="true"] .filters__unselect {
35+
position: absolute;
36+
z-index: 1;
37+
top: calc(-1 * var(--space-smaller));
38+
right: calc(-1 * var(--space-smaller));
39+
40+
display: block;
41+
padding: 2px;
42+
43+
font-size: var(--font-size-smaller);
44+
45+
background-color: var(--color-accent-base);
46+
border: 1px solid var(--color-accent-border);
47+
border-radius: 100%;
48+
}

src/assets/stylesheets/components/sources.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,58 @@
4747
border-radius: var(--border-radius);
4848
}
4949

50+
.source--button {
51+
max-width: 17rem;
52+
height: 5rem;
53+
padding: var(--space-small);
54+
55+
text-align: left;
56+
57+
text-decoration: none;
58+
59+
cursor: pointer;
60+
}
61+
5062
.source__image {
5163
width: 64px;
5264
max-width: none;
5365

5466
background-color: var(--color-base);
5567
border-radius: var(--border-radius);
5668
}
69+
70+
.source__body {
71+
display: block;
72+
}
73+
74+
.source__name {
75+
overflow: hidden;
76+
77+
display: -webkit-box;
78+
max-height: 2lh;
79+
80+
color: var(--color-text);
81+
font-weight: 600;
82+
text-overflow: ellipsis;
83+
84+
-webkit-line-clamp: 2;
85+
-webkit-box-orient: vertical;
86+
}
87+
88+
.source__count {
89+
display: block;
90+
91+
color: var(--color-grey-text);
92+
text-align: right;
93+
}
94+
95+
.source__unread-dot {
96+
display: inline-block;
97+
width: 10px;
98+
height: 10px;
99+
100+
vertical-align: middle;
101+
102+
background-color: var(--color-accent-solid);
103+
border-radius: 50%;
104+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#stream-timeline {
2+
transition: opacity 0.2s ease-in-out;
3+
}
4+
5+
#stream-timeline[aria-busy="true"] {
6+
opacity: 0.6;
7+
}

src/controllers/Streams.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ public function show(Request $request): Response
8585

8686
auth\Access::require($user, 'view', $stream);
8787

88-
$stream_view = models\StreamView::buildFromRequest($stream, $request);
88+
$stream_view = models\StreamView::buildFromRequest($stream, $user, $request);
8989

9090
return Response::ok('streams/show.html.twig', [
9191
'stream_view' => $stream_view,

src/models/Stream.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ public function publicationFrequencyPerYear(): int
134134
* context_user?: ?User,
135135
* at?: \DateTimeImmutable,
136136
* days?: int,
137+
* source?: ?Collection,
138+
* status?: string,
137139
* } $options
138140
*
139141
* @return Link[]
@@ -143,6 +145,34 @@ public function links(array $options = []): array
143145
return Link::listByStream($this, $options);
144146
}
145147

148+
/**
149+
* @param array{
150+
* context_user?: ?User,
151+
* at?: \DateTimeImmutable,
152+
* days?: int,
153+
* } $options
154+
*
155+
* @return array<string, int>
156+
*/
157+
public function countLinksPerDay(array $options = []): array
158+
{
159+
return Link::countByStreamPerDay($this, $options);
160+
}
161+
162+
/**
163+
* @param array{
164+
* context_user?: ?User,
165+
* at?: \DateTimeImmutable,
166+
* days?: int,
167+
* } $options
168+
*
169+
* @return array<string, array{int, int}>
170+
*/
171+
public function countLinksPerSource(array $options = []): array
172+
{
173+
return Link::countByStreamPerSource($this, $options);
174+
}
175+
146176
/**
147177
* Return a tag URI that can be used as Atom id
148178
*

0 commit comments

Comments
 (0)