From b3b6ba02ea5ac2df15499fc4ac7d2458def7c3eb Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Thu, 23 Jul 2026 18:41:51 +0700 Subject: [PATCH 1/5] feat(feed): add responsive pagination defaults --- public/translations/ar/default.json | 5 +- public/translations/bn/default.json | 5 +- public/translations/cs/default.json | 5 +- public/translations/da/default.json | 5 +- public/translations/de/default.json | 5 +- public/translations/el/default.json | 5 +- public/translations/en/default.json | 5 +- public/translations/es/default.json | 5 +- public/translations/fa/default.json | 5 +- public/translations/fi/default.json | 5 +- public/translations/fil/default.json | 5 +- public/translations/fr/default.json | 5 +- public/translations/he/default.json | 5 +- public/translations/hi/default.json | 5 +- public/translations/hu/default.json | 5 +- public/translations/id/default.json | 5 +- public/translations/it/default.json | 5 +- public/translations/ja/default.json | 5 +- public/translations/ko/default.json | 5 +- public/translations/mr/default.json | 5 +- public/translations/nl/default.json | 5 +- public/translations/no/default.json | 5 +- public/translations/pl/default.json | 5 +- public/translations/pt/default.json | 5 +- public/translations/ro/default.json | 5 +- public/translations/ru/default.json | 5 +- public/translations/sq/default.json | 5 +- public/translations/sv/default.json | 5 +- public/translations/te/default.json | 5 +- public/translations/th/default.json | 5 +- public/translations/tr/default.json | 5 +- public/translations/uk/default.json | 5 +- public/translations/ur/default.json | 5 +- public/translations/vi/default.json | 5 +- public/translations/zh/default.json | 5 +- .../feed-footer/feed-footer.module.css | 10 ++- src/components/feed-footer/feed-footer.tsx | 16 ++++- .../feed-footer/feed-pagination.test.tsx | 61 +++++++++++++++++++ .../feed-footer/feed-pagination.tsx | 26 ++++++++ src/hooks/use-feed-pagination.test.ts | 14 +++++ src/hooks/use-feed-pagination.ts | 12 ++++ src/stores/use-content-options-store.test.ts | 37 +++++++++++ src/stores/use-content-options-store.ts | 4 ++ src/views/all/all.tsx | 6 +- src/views/community/community.tsx | 13 +++- src/views/domain/domain.tsx | 7 ++- src/views/home/home.tsx | 8 ++- src/views/mod/mod.tsx | 6 +- .../content-options/content-options.tsx | 22 +++++++ 49 files changed, 370 insertions(+), 47 deletions(-) create mode 100644 src/components/feed-footer/feed-pagination.test.tsx create mode 100644 src/components/feed-footer/feed-pagination.tsx create mode 100644 src/hooks/use-feed-pagination.test.ts create mode 100644 src/hooks/use-feed-pagination.ts create mode 100644 src/stores/use-content-options-store.test.ts diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index 8b10cf9b..281111a4 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "الاحتفاظ بكليهما", "keep_community": "الاحتفاظ بـ {{communityAddress}}", "directory_automatic_switch_notice": "تغيّر s/{{directoryCode}}، لذا حوّل Seedit اشتراكك من {{fromAddress}} إلى {{toAddress}}.", - "dismiss": "تجاهل" + "dismiss": "تجاهل", + "load_more": "تحميل المزيد", + "enable_infinite_feed": "تمكين الخلاصة اللانهائية", + "feeds": "الخلاصات" } diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index 60f42b5a..3d89237d 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "দুটিই রাখুন", "keep_community": "{{communityAddress}} রাখুন", "directory_automatic_switch_notice": "s/{{directoryCode}} পরিবর্তিত হয়েছে, তাই Seedit আপনার সাবস্ক্রিপশন {{fromAddress}} থেকে {{toAddress}}-এ পরিবর্তন করেছে।", - "dismiss": "খারিজ করুন" + "dismiss": "খারিজ করুন", + "load_more": "আরও লোড করুন", + "enable_infinite_feed": "অসীম ফিড চালু করুন", + "feeds": "ফিডসমূহ" } diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 13ef786b..225b9bdf 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "ponechat obě", "keep_community": "ponechat {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} se změnilo, takže Seedit převedl vaše předplatné z {{fromAddress}} na {{toAddress}}.", - "dismiss": "Zavřít" + "dismiss": "Zavřít", + "load_more": "Načíst další", + "enable_infinite_feed": "Povolit nekonečný kanál", + "feeds": "kanály" } diff --git a/public/translations/da/default.json b/public/translations/da/default.json index 48e24dfb..c64557cc 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "behold begge", "keep_community": "behold {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} er ændret, så Seedit skiftede dit abonnement fra {{fromAddress}} til {{toAddress}}.", - "dismiss": "Luk" + "dismiss": "Luk", + "load_more": "Indlæs flere", + "enable_infinite_feed": "Aktivér uendeligt feed", + "feeds": "feeds" } diff --git a/public/translations/de/default.json b/public/translations/de/default.json index bcb825c5..87994293 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "beide behalten", "keep_community": "{{communityAddress}} behalten", "directory_automatic_switch_notice": "s/{{directoryCode}} hat sich geändert, daher hat Seedit dein Abonnement von {{fromAddress}} zu {{toAddress}} gewechselt.", - "dismiss": "Ausblenden" + "dismiss": "Ausblenden", + "load_more": "Mehr laden", + "enable_infinite_feed": "Endlosen Feed aktivieren", + "feeds": "Feeds" } diff --git a/public/translations/el/default.json b/public/translations/el/default.json index 3f02fa8f..af2c88a2 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "διατήρηση και των δύο", "keep_community": "διατήρηση του {{communityAddress}}", "directory_automatic_switch_notice": "Το s/{{directoryCode}} άλλαξε, οπότε το Seedit μετέφερε τη συνδρομή σας από το {{fromAddress}} στο {{toAddress}}.", - "dismiss": "Κλείσιμο" + "dismiss": "Κλείσιμο", + "load_more": "Φόρτωση περισσότερων", + "enable_infinite_feed": "Ενεργοποίηση ατελείωτης ροής", + "feeds": "ροές" } diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 3048f3f3..781c7bf0 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "keep both", "keep_community": "keep {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} changed, so Seedit switched your subscription from {{fromAddress}} to {{toAddress}}.", - "dismiss": "dismiss" + "dismiss": "dismiss", + "load_more": "Load more", + "enable_infinite_feed": "Enable infinite feed", + "feeds": "feeds" } diff --git a/public/translations/es/default.json b/public/translations/es/default.json index 1bf3ce3c..117cf39a 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "conservar ambas", "keep_community": "conservar {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} cambió, así que Seedit cambió tu suscripción de {{fromAddress}} a {{toAddress}}.", - "dismiss": "descartar" + "dismiss": "descartar", + "load_more": "Cargar más", + "enable_infinite_feed": "Activar feed infinito", + "feeds": "feeds" } diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index 5970c882..03fbc3c4 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "هر دو را نگه دارید", "keep_community": "حفظ {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} تغییر کرد، بنابراین Seedit اشتراک شما را از {{fromAddress}} به {{toAddress}} منتقل کرد.", - "dismiss": "بستن" + "dismiss": "بستن", + "load_more": "بارگذاری بیشتر", + "enable_infinite_feed": "فعال‌سازی خوراک بی‌نهایت", + "feeds": "فیدها" } diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index 83414f72..a33e3112 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "pidä molemmat", "keep_community": "säilytä {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} muuttui, joten Seedit vaihtoi tilauksesi yhteisöstä {{fromAddress}} yhteisöön {{toAddress}}.", - "dismiss": "Sulje" + "dismiss": "Sulje", + "load_more": "Lataa lisää", + "enable_infinite_feed": "Ota loputon syöte käyttöön", + "feeds": "syötteet" } diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index 6a9124da..add82356 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "panatilihin pareho", "keep_community": "panatilihin ang {{communityAddress}}", "directory_automatic_switch_notice": "Nagbago ang s/{{directoryCode}}, kaya inilipat ng Seedit ang subscription mo mula {{fromAddress}} papuntang {{toAddress}}.", - "dismiss": "Isara" + "dismiss": "Isara", + "load_more": "Mag-load pa", + "enable_infinite_feed": "I-enable ang walang katapusang feed", + "feeds": "mga feed" } diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index b2a0368f..c0209b12 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "garder les deux", "keep_community": "conserver {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} a changé, Seedit a donc remplacé votre abonnement à {{fromAddress}} par {{toAddress}}.", - "dismiss": "ignorer" + "dismiss": "ignorer", + "load_more": "Charger plus", + "enable_infinite_feed": "Activer le fil infini", + "feeds": "fils" } diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 8607d672..75e77ed5 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "להשאיר את שניהם", "keep_community": "להשאיר את {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} השתנה, ולכן Seedit החליף את המינוי שלך מ־{{fromAddress}} ל־{{toAddress}}.", - "dismiss": "סגירה" + "dismiss": "סגירה", + "load_more": "טען עוד", + "enable_infinite_feed": "הפעלת פיד אינסופי", + "feeds": "פידים" } diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index 97f5b15b..ead28858 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "दोनों रखें", "keep_community": "{{communityAddress}} को रखें", "directory_automatic_switch_notice": "s/{{directoryCode}} बदल गया, इसलिए Seedit ने आपकी सदस्यता {{fromAddress}} से {{toAddress}} पर बदल दी।", - "dismiss": "बंद करें" + "dismiss": "बंद करें", + "load_more": "और लोड करें", + "enable_infinite_feed": "अनंत फ़ीड सक्षम करें", + "feeds": "फ़ीड" } diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index c361c243..12d1e61f 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "mindkettő megtartása", "keep_community": "{{communityAddress}} megtartása", "directory_automatic_switch_notice": "Az s/{{directoryCode}} megváltozott, ezért a Seedit a feliratkozásodat {{fromAddress}} címről {{toAddress}} címre váltotta.", - "dismiss": "Bezárás" + "dismiss": "Bezárás", + "load_more": "Továbbiak betöltése", + "enable_infinite_feed": "Végtelen hírfolyam engedélyezése", + "feeds": "hírfolyamok" } diff --git a/public/translations/id/default.json b/public/translations/id/default.json index 4509770f..5f75428c 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "simpan keduanya", "keep_community": "pertahankan {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} berubah, jadi Seedit mengalihkan langganan Anda dari {{fromAddress}} ke {{toAddress}}.", - "dismiss": "Tutup" + "dismiss": "Tutup", + "load_more": "Muat lebih banyak", + "enable_infinite_feed": "Aktifkan feed tanpa batas", + "feeds": "feed" } diff --git a/public/translations/it/default.json b/public/translations/it/default.json index b9c16378..09db377e 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "mantieni entrambe", "keep_community": "mantieni {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} è cambiato, quindi Seedit ha spostato la tua iscrizione da {{fromAddress}} a {{toAddress}}.", - "dismiss": "ignora" + "dismiss": "ignora", + "load_more": "Carica altro", + "enable_infinite_feed": "Abilita il feed infinito", + "feeds": "feed" } diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index 2d0d69d0..306235ba 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "両方を維持", "keep_community": "{{communityAddress}}を維持する", "directory_automatic_switch_notice": "s/{{directoryCode}} が変更されたため、Seedit がサブスクリプションを {{fromAddress}} から {{toAddress}} に切り替えました。", - "dismiss": "閉じる" + "dismiss": "閉じる", + "load_more": "さらに読み込む", + "enable_infinite_feed": "無限フィードを有効にする", + "feeds": "フィード" } diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index 53219c13..d7da48e8 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "둘 다 유지", "keep_community": "{{communityAddress}} 유지", "directory_automatic_switch_notice": "s/{{directoryCode}}의 추천이 변경되어 Seedit에서 구독 대상을 {{fromAddress}}에서 {{toAddress}}로 전환했습니다.", - "dismiss": "닫기" + "dismiss": "닫기", + "load_more": "더 불러오기", + "enable_infinite_feed": "무한 피드 활성화", + "feeds": "피드" } diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index bd121bc2..9b7efda9 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "दोन्ही ठेवा", "keep_community": "{{communityAddress}} ठेवा", "directory_automatic_switch_notice": "s/{{directoryCode}} बदलले, त्यामुळे Seedit ने तुमचे सदस्यत्व {{fromAddress}} वरून {{toAddress}} वर बदलले.", - "dismiss": "बंद करा" + "dismiss": "बंद करा", + "load_more": "आणखी लोड करा", + "enable_infinite_feed": "अमर्याद फीड सुरू करा", + "feeds": "फीड" } diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index 9a8a0a40..4564f109 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "beide behouden", "keep_community": "{{communityAddress}} behouden", "directory_automatic_switch_notice": "s/{{directoryCode}} is gewijzigd, dus Seedit heeft je abonnement van {{fromAddress}} naar {{toAddress}} overgezet.", - "dismiss": "Sluiten" + "dismiss": "Sluiten", + "load_more": "Meer laden", + "enable_infinite_feed": "Oneindige feed inschakelen", + "feeds": "feeds" } diff --git a/public/translations/no/default.json b/public/translations/no/default.json index 3579c224..e2516274 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "behold begge", "keep_community": "behold {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} ble endret, så Seedit byttet abonnementet ditt fra {{fromAddress}} til {{toAddress}}.", - "dismiss": "Lukk" + "dismiss": "Lukk", + "load_more": "Last inn mer", + "enable_infinite_feed": "Aktiver uendelig feed", + "feeds": "feeder" } diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index 6a2e1dec..160bff38 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "zachowaj obie", "keep_community": "zachowaj {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} zmieniło się, więc Seedit zmienił Twoją subskrypcję z {{fromAddress}} na {{toAddress}}.", - "dismiss": "Zamknij" + "dismiss": "Zamknij", + "load_more": "Załaduj więcej", + "enable_infinite_feed": "Włącz nieskończony kanał", + "feeds": "kanały" } diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index 41f5e0e4..94e331b7 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "manter ambas", "keep_community": "manter {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} mudou, então o Seedit alterou sua assinatura de {{fromAddress}} para {{toAddress}}.", - "dismiss": "dispensar" + "dismiss": "dispensar", + "load_more": "Carregar mais", + "enable_infinite_feed": "Ativar feed infinito", + "feeds": "feeds" } diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index 78ef1a09..1ac2530d 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "păstrează-le pe ambele", "keep_community": "păstrează {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} s-a schimbat, așa că Seedit ți-a mutat abonamentul de la {{fromAddress}} la {{toAddress}}.", - "dismiss": "Închide" + "dismiss": "Închide", + "load_more": "Încarcă mai multe", + "enable_infinite_feed": "Activează fluxul infinit", + "feeds": "fluxuri" } diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index cca127d6..418abfd4 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "оставить оба", "keep_community": "оставить {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} изменился, поэтому Seedit переключил вашу подписку с {{fromAddress}} на {{toAddress}}.", - "dismiss": "Закрыть" + "dismiss": "Закрыть", + "load_more": "Загрузить ещё", + "enable_infinite_feed": "Включить бесконечную ленту", + "feeds": "ленты" } diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index 7af13779..e73557f7 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "mbaji të dyja", "keep_community": "mbaj {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} ndryshoi, ndaj Seedit e kaloi abonimin tënd nga {{fromAddress}} te {{toAddress}}.", - "dismiss": "Mbyll" + "dismiss": "Mbyll", + "load_more": "Ngarko më shumë", + "enable_infinite_feed": "Aktivizo feed-in e pafund", + "feeds": "prurjet" } diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index 58697e2b..24a4ab82 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "behåll båda", "keep_community": "behåll {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} ändrades, så Seedit bytte din prenumeration från {{fromAddress}} till {{toAddress}}.", - "dismiss": "Stäng" + "dismiss": "Stäng", + "load_more": "Ladda fler", + "enable_infinite_feed": "Aktivera oändligt flöde", + "feeds": "flöden" } diff --git a/public/translations/te/default.json b/public/translations/te/default.json index 7c4d17fd..dd602260 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "రెండింటినీ ఉంచు", "keep_community": "{{communityAddress}}ను అలాగే ఉంచండి", "directory_automatic_switch_notice": "s/{{directoryCode}} మారింది, కాబట్టి Seedit మీ సబ్‌స్క్రిప్షన్‌ను {{fromAddress}} నుండి {{toAddress}}కి మార్చింది.", - "dismiss": "మూసివేయి" + "dismiss": "మూసివేయి", + "load_more": "మరిన్ని లోడ్ చేయండి", + "enable_infinite_feed": "అనంత ఫీడ్‌ను ప్రారంభించండి", + "feeds": "ఫీడ్‌లు" } diff --git a/public/translations/th/default.json b/public/translations/th/default.json index 7c7a1646..546b8b77 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "เก็บไว้ทั้งคู่", "keep_community": "เก็บ {{communityAddress}} ไว้", "directory_automatic_switch_notice": "s/{{directoryCode}} มีการเปลี่ยนแปลง ดังนั้น Seedit จึงเปลี่ยนการสมัครของคุณจาก {{fromAddress}} เป็น {{toAddress}}", - "dismiss": "ปิด" + "dismiss": "ปิด", + "load_more": "โหลดเพิ่มเติม", + "enable_infinite_feed": "เปิดใช้งานฟีดแบบไม่สิ้นสุด", + "feeds": "ฟีด" } diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index a721c0ff..34e69fda 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "ikisini de tut", "keep_community": "{{communityAddress}} topluluğunu koru", "directory_automatic_switch_notice": "s/{{directoryCode}} değişti, bu nedenle Seedit aboneliğinizi {{fromAddress}} adresinden {{toAddress}} adresine geçirdi.", - "dismiss": "Kapat" + "dismiss": "Kapat", + "load_more": "Daha fazla yükle", + "enable_infinite_feed": "Sonsuz akışı etkinleştir", + "feeds": "akışlar" } diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index fb27d241..4b9ec43e 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "залишити обидві", "keep_community": "залишити {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} змінився, тому Seedit перемкнув вашу підписку з {{fromAddress}} на {{toAddress}}.", - "dismiss": "Закрити" + "dismiss": "Закрити", + "load_more": "Завантажити ще", + "enable_infinite_feed": "Увімкнути нескінченну стрічку", + "feeds": "стрічки" } diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index c9ee5d5a..5f9515bb 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "دونوں رکھیں", "keep_community": "{{communityAddress}} برقرار رکھیں", "directory_automatic_switch_notice": "s/{{directoryCode}} تبدیل ہو گیا، اس لیے Seedit نے آپ کی سبسکرپشن {{fromAddress}} سے {{toAddress}} پر منتقل کر دی۔", - "dismiss": "بند کریں" + "dismiss": "بند کریں", + "load_more": "مزید لوڈ کریں", + "enable_infinite_feed": "لامحدود فیڈ فعال کریں", + "feeds": "فیڈز" } diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index 75033bdd..4d1102d9 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "giữ cả hai", "keep_community": "giữ {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} đã thay đổi, vì vậy Seedit đã chuyển đăng ký của bạn từ {{fromAddress}} sang {{toAddress}}.", - "dismiss": "Đóng" + "dismiss": "Đóng", + "load_more": "Tải thêm", + "enable_infinite_feed": "Bật bảng tin vô hạn", + "feeds": "bảng tin" } diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 0c6fd8f0..48cda751 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -401,5 +401,8 @@ "keep_both_communities": "两者都保留", "keep_community": "保留 {{communityAddress}}", "directory_automatic_switch_notice": "s/{{directoryCode}} 已更改,因此 Seedit 已将你的订阅从 {{fromAddress}} 切换到 {{toAddress}}。", - "dismiss": "关闭" + "dismiss": "关闭", + "load_more": "加载更多", + "enable_infinite_feed": "启用无限信息流", + "feeds": "信息流" } diff --git a/src/components/feed-footer/feed-footer.module.css b/src/components/feed-footer/feed-footer.module.css index 432d3e84..4efb9686 100644 --- a/src/components/feed-footer/feed-footer.module.css +++ b/src/components/feed-footer/feed-footer.module.css @@ -21,13 +21,17 @@ padding-bottom: 15px; } +.loadMoreContainer { + padding-bottom: 15px; +} + @media (max-width: 640px) { - .morePostsSuggestion { + .morePostsSuggestion, .loadMoreContainer { padding-bottom: 10px; } } -.morePostsSuggestion a, .link { +.morePostsSuggestion a, .link, .loadMore { color: var(--text-primary); text-decoration: none; padding: 1px 4px; @@ -40,7 +44,7 @@ color: var(--link-primary); } -.morePostsSuggestion a:hover, .link:hover { +.morePostsSuggestion a:hover, .link:hover, .loadMore:hover { border: var(--pagination-button-border-hover); } diff --git a/src/components/feed-footer/feed-footer.tsx b/src/components/feed-footer/feed-footer.tsx index 57fa31b4..0096b802 100644 --- a/src/components/feed-footer/feed-footer.tsx +++ b/src/components/feed-footer/feed-footer.tsx @@ -6,6 +6,8 @@ import { isAllView, isModView } from '../../lib/utils/view-utils'; import { useCommunityIdentifiers } from '../../hooks/use-community-identifier'; import { useFeedStateString } from '../../hooks/use-state-string'; import EmptyFeedMessage from '../empty-feed-message/empty-feed-message'; +import { useInfiniteFeedEnabled } from '../../hooks/use-feed-pagination'; +import FeedPagination from './feed-pagination'; import LoadingEllipsis from '../loading-ellipsis'; import { shouldShowEmptyFeed } from './feed-footer-utils'; import styles from './feed-footer.module.css'; @@ -26,6 +28,7 @@ interface FeedFooterProps { isSearching?: boolean; showNoResults?: boolean; onClearSearch?: () => void; + onLoadMore: () => void; } const FeedFooter = ({ @@ -41,6 +44,7 @@ const FeedFooter = ({ isSearching, showNoResults, onClearSearch, + onLoadMore, }: FeedFooterProps) => { let footerContent; const { t } = useTranslation(); @@ -48,6 +52,7 @@ const FeedFooter = ({ const location = useLocation(); const isInModView = isModView(location.pathname); const isInAllView = isAllView(location.pathname); + const infiniteFeedEnabled = useInfiniteFeedEnabled(); const feedCommunityIdentifiers = useCommunityIdentifiers(communityAddresses); const { communities } = useCommunities({ communities: feedCommunityIdentifiers }); @@ -181,13 +186,20 @@ const FeedFooter = ({ ) ) : !searchQuery ? ( - + infiniteFeedEnabled || !hasMore ? ( + + ) : null ) : null} ); } - return
{footerContent}
; + return ( +
+ {footerContent} + +
+ ); }; export default React.memo(FeedFooter); diff --git a/src/components/feed-footer/feed-pagination.test.tsx b/src/components/feed-footer/feed-pagination.test.tsx new file mode 100644 index 00000000..ee0aae71 --- /dev/null +++ b/src/components/feed-footer/feed-pagination.test.tsx @@ -0,0 +1,61 @@ +// @vitest-environment jsdom + +import * as React from 'react'; +import { createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import FeedPagination from './feed-pagination'; + +(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; +const act = (React as { act?: (callback: () => void | Promise) => void | Promise }).act as (callback: () => void | Promise) => void | Promise; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +const paginationState = vi.hoisted(() => ({ infiniteFeedEnabled: false })); + +vi.mock('../../hooks/use-feed-pagination', () => ({ + useInfiniteFeedEnabled: () => paginationState.infiniteFeedEnabled, +})); + +describe('FeedPagination', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + paginationState.infiniteFeedEnabled = false; + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => root.unmount()); + container.remove(); + }); + + it('shows a manual load-more button by default and invokes it', async () => { + const onLoadMore = vi.fn(); + + await act(async () => { + root.render(createElement(FeedPagination, { feedLength: 25, hasMore: true, onLoadMore })); + }); + + const button = container.querySelector('button'); + expect(button?.textContent).toBe('load_more'); + + await act(async () => button?.click()); + expect(onLoadMore).toHaveBeenCalledOnce(); + }); + + it('hides manual pagination when infinite feed is enabled', async () => { + paginationState.infiniteFeedEnabled = true; + + await act(async () => { + root.render(createElement(FeedPagination, { feedLength: 25, hasMore: true, onLoadMore: vi.fn() })); + }); + + expect(container.querySelector('button')).toBeNull(); + }); +}); diff --git a/src/components/feed-footer/feed-pagination.tsx b/src/components/feed-footer/feed-pagination.tsx new file mode 100644 index 00000000..9c443963 --- /dev/null +++ b/src/components/feed-footer/feed-pagination.tsx @@ -0,0 +1,26 @@ +import { useTranslation } from 'react-i18next'; +import { useInfiniteFeedEnabled } from '../../hooks/use-feed-pagination'; +import styles from './feed-footer.module.css'; + +interface FeedPaginationProps { + feedLength: number; + hasMore: boolean; + onLoadMore: () => void; +} + +const FeedPagination = ({ feedLength, hasMore, onLoadMore }: FeedPaginationProps) => { + const { t } = useTranslation(); + const infiniteFeedEnabled = useInfiniteFeedEnabled(); + + if (infiniteFeedEnabled || !hasMore || feedLength === 0) return null; + + return ( +
+ +
+ ); +}; + +export default FeedPagination; diff --git a/src/hooks/use-feed-pagination.test.ts b/src/hooks/use-feed-pagination.test.ts new file mode 100644 index 00000000..a775b616 --- /dev/null +++ b/src/hooks/use-feed-pagination.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { getInfiniteFeedEnabled } from './use-feed-pagination'; + +describe('getInfiniteFeedEnabled', () => { + it('defaults to infinite loading on mobile and manual pagination on desktop', () => { + expect(getInfiniteFeedEnabled(null, true)).toBe(true); + expect(getInfiniteFeedEnabled(null, false)).toBe(false); + }); + + it('uses an explicit preference on every viewport', () => { + expect(getInfiniteFeedEnabled(false, true)).toBe(false); + expect(getInfiniteFeedEnabled(true, false)).toBe(true); + }); +}); diff --git a/src/hooks/use-feed-pagination.ts b/src/hooks/use-feed-pagination.ts new file mode 100644 index 00000000..a6285c50 --- /dev/null +++ b/src/hooks/use-feed-pagination.ts @@ -0,0 +1,12 @@ +import useContentOptionsStore from '../stores/use-content-options-store'; +import useIsMobile from './use-is-mobile'; + +export const FEED_POSTS_PER_PAGE = 25; + +export const getInfiniteFeedEnabled = (preference: boolean | null, isMobile: boolean) => preference ?? isMobile; + +export const useInfiniteFeedEnabled = () => { + const preference = useContentOptionsStore((state) => state.infiniteFeedEnabled); + const isMobile = useIsMobile(); + return getInfiniteFeedEnabled(preference, isMobile); +}; diff --git a/src/stores/use-content-options-store.test.ts b/src/stores/use-content-options-store.test.ts new file mode 100644 index 00000000..dc54baf5 --- /dev/null +++ b/src/stores/use-content-options-store.test.ts @@ -0,0 +1,37 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const createStorage = () => { + const values = new Map(); + return { + clear: () => values.clear(), + getItem: (key: string) => values.get(key) ?? null, + key: (index: number) => [...values.keys()][index] ?? null, + get length() { + return values.size; + }, + removeItem: (key: string) => values.delete(key), + setItem: (key: string, value: string) => values.set(key, value), + } satisfies Storage; +}; + +describe('useContentOptionsStore', () => { + beforeEach(() => { + vi.resetModules(); + vi.stubGlobal('localStorage', createStorage()); + }); + + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('defaults infinite feed to the responsive policy and persists explicit changes', async () => { + const { default: useContentOptionsStore } = await import('./use-content-options-store'); + + expect(useContentOptionsStore.getState().infiniteFeedEnabled).toBeNull(); + + useContentOptionsStore.getState().setInfiniteFeedEnabled(true); + + expect(useContentOptionsStore.getState().infiniteFeedEnabled).toBe(true); + expect(localStorage.getItem('content-options')).toContain('"infiniteFeedEnabled":true'); + }); +}); diff --git a/src/stores/use-content-options-store.ts b/src/stores/use-content-options-store.ts index d4f60063..3f991d68 100644 --- a/src/stores/use-content-options-store.ts +++ b/src/stores/use-content-options-store.ts @@ -14,6 +14,8 @@ interface ContentOptionsState { setAutoplayVideosOnComments: (autoplay: boolean) => void; muteVideosOnComments: boolean; setMuteVideosOnComments: (mute: boolean) => void; + infiniteFeedEnabled: boolean | null; + setInfiniteFeedEnabled: (enabled: boolean) => void; } interface ContentOptionsStore extends ContentOptionsState { @@ -37,6 +39,7 @@ const useContentOptionsStore = create()( mediaPreviewOption: 'autoExpandAll', autoplayVideosOnComments: true, muteVideosOnComments: true, + infiniteFeedEnabled: null, setBlurNsfwThumbnails: (blur) => set({ blurNsfwThumbnails: blur }), setHideNsfwCommunities: (hide) => set({ hideNsfwCommunities: hide }), setHasAcceptedWarning: (value) => set({ hasAcceptedWarning: value }), @@ -46,6 +49,7 @@ const useContentOptionsStore = create()( setMediaPreviewOption: (option) => set({ mediaPreviewOption: option }), setAutoplayVideosOnComments: (autoplay) => set({ autoplayVideosOnComments: autoplay }), setMuteVideosOnComments: (mute) => set({ muteVideosOnComments: mute }), + setInfiniteFeedEnabled: (enabled) => set({ infiniteFeedEnabled: enabled }), }), { name: 'content-options', diff --git a/src/views/all/all.tsx b/src/views/all/all.tsx index e78a0b8c..4c1ee25b 100644 --- a/src/views/all/all.tsx +++ b/src/views/all/all.tsx @@ -7,6 +7,7 @@ import { commentMatchesPattern } from '../../lib/utils/pattern-utils'; import useFeedFiltersStore from '../../stores/use-feed-filters-store'; import { useDefaultSubscriptionAddresses } from '../../hooks/use-default-subscriptions'; import useTimeFilter, { isValidTimeFilterName } from '../../hooks/use-time-filter'; +import { FEED_POSTS_PER_PAGE, useInfiniteFeedEnabled } from '../../hooks/use-feed-pagination'; import FeedFooter from '../../components/feed-footer'; import { getCommunityIdentifiers } from '../../hooks/use-community-identifier'; import LoadingEllipsis from '../../components/loading-ellipsis'; @@ -48,12 +49,14 @@ const All = () => { const currentTimeFilterName = params.timeFilterName || timeFilterName || 'hot'; const { isSearching } = useFeedFiltersStore(); + const infiniteFeedEnabled = useInfiniteFeedEnabled(); const [showNoResults, setShowNoResults] = useState(false); const [searchAttemptCompleted, setSearchAttemptCompleted] = useState(false); const feedOptions = useMemo(() => { const options: any = { newerThan: searchQuery ? 0 : timeFilterSeconds, + postsPerPage: FEED_POSTS_PER_PAGE, sortType, communities: getCommunityIdentifiers(communityAddresses), }; @@ -174,6 +177,7 @@ const All = () => { searchQuery: searchQuery, isSearching, showNoResults, + onLoadMore: combinedLoadMore, }; const handleClearSearch = () => { @@ -228,7 +232,7 @@ const All = () => { itemContent={(index, post) => } useWindowScroll={true} components={{ Footer: () => }} - endReached={combinedLoadMore} + endReached={infiniteFeedEnabled ? combinedLoadMore : undefined} ref={virtuosoRef} restoreStateFrom={lastVirtuosoState} initialScrollTop={lastVirtuosoState?.scrollTop} diff --git a/src/views/community/community.tsx b/src/views/community/community.tsx index 5f87ed10..2af9b0aa 100644 --- a/src/views/community/community.tsx +++ b/src/views/community/community.tsx @@ -16,9 +16,11 @@ import useIsCommunityOffline from '../../hooks/use-is-community-offline'; import useResolvedCommunityRoute from '../../hooks/use-resolved-community-route'; import { getCommunityPath, isResolvableCommunityAddress } from '../../lib/utils/community-route-utils'; import useTimeFilter, { isValidTimeFilterName } from '../../hooks/use-time-filter'; +import { FEED_POSTS_PER_PAGE, useInfiniteFeedEnabled } from '../../hooks/use-feed-pagination'; import { getCommunityIdentifier, getCommunityIdentifiers } from '../../hooks/use-community-identifier'; import ErrorDisplay from '../../components/error-display'; import EmptyFeedMessage from '../../components/empty-feed-message/empty-feed-message'; +import FeedPagination from '../../components/feed-footer/feed-pagination'; import LoadingEllipsis from '../../components/loading-ellipsis'; import Over18Warning from '../../components/over-18-warning'; import Post from '../../components/post'; @@ -42,6 +44,7 @@ interface FooterProps { searchQuery: string; isSearching: boolean; setSearchParams: ReturnType[1]; + onLoadMore: () => void; } const Footer = ({ @@ -58,12 +61,14 @@ const Footer = ({ searchQuery, isSearching, setSearchParams, + onLoadMore, }: FooterProps) => { const { t } = useTranslation(); let footerFirstLine; let footerSecondLine; const feedStateString = useFeedStateString(communityAddresses); const loadingStateString = feedStateString || t('loading'); + const infiniteFeedEnabled = useInfiniteFeedEnabled(); const [showNoResults, setShowNoResults] = useState(false); const [searchAttemptCompleted, setSearchAttemptCompleted] = useState(false); @@ -189,7 +194,7 @@ const Footer = ({ footerFirstLine = ; } else if (feedLength === 0 || !isOnline) { footerFirstLine = loadingString; - } else if (hasMore) { + } else if (hasMore && infiniteFeedEnabled) { footerFirstLine = loadingString; } @@ -217,6 +222,7 @@ const Footer = ({ )} {footerSecondLine} + ); }; @@ -256,10 +262,12 @@ const CommunityView = () => { const timeFilterName = params.timeFilterName || 'all'; const { timeFilterSeconds } = useTimeFilter(); const { isSearching } = useFeedFiltersStore(); + const infiniteFeedEnabled = useInfiniteFeedEnabled(); const feedOptions = useMemo(() => { const options: any = { communities: getCommunityIdentifiers(communityAddresses), + postsPerPage: FEED_POSTS_PER_PAGE, sortType, newerThan: searchQuery ? 0 : timeFilterSeconds, }; @@ -330,6 +338,7 @@ const CommunityView = () => { searchQuery, isSearching, setSearchParams, + onLoadMore: loadMore, }; // scrolling position state for virtuoso feed @@ -401,7 +410,7 @@ const CommunityView = () => { itemContent={(index, post) => } useWindowScroll={true} components={{ Footer: () =>