diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index b02afd4e..846cc7f3 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "تفعيل التمرير اللانهائي", "prev": "السابق", "next": "التالي", - "bottom": "أسفل" + "bottom": "أسفل", + "enable_infinite_scroll_tip": "لا ينطبق على اللوحات المتعددة، التي تستخدم التمرير اللانهائي دائماً." } diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index 07b0a7d5..1540846b 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "অসীম স্ক্রল সক্ষম করুন", "prev": "পূর্ববর্তী", "next": "পরবর্তী", - "bottom": "নিচ" + "bottom": "নিচ", + "enable_infinite_scroll_tip": "মাল্টিবোর্ডে প্রযোজ্য নয়, যা সবসময় অসীম স্ক্রল ব্যবহার করে।" } diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 0b074793..08f826df 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Povolit nekonečné posouvání", "prev": "Předchozí", "next": "Další", - "bottom": "Dole" + "bottom": "Dole", + "enable_infinite_scroll_tip": "Netýká se multidesek, které vždy používají nekonečné posouvání." } diff --git a/public/translations/da/default.json b/public/translations/da/default.json index 05cb798b..c8c6c0c2 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Aktiver uendelig rulning", "prev": "Forrige", "next": "Næste", - "bottom": "Bund" + "bottom": "Bund", + "enable_infinite_scroll_tip": "Gælder ikke for multiboard, som altid bruger uendelig scroll." } diff --git a/public/translations/de/default.json b/public/translations/de/default.json index 62582ae3..1c0868aa 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Unendliches Scrollen aktivieren", "prev": "Zurück", "next": "Weiter", - "bottom": "Unten" + "bottom": "Unten", + "enable_infinite_scroll_tip": "Gilt nicht für Multiboards, die immer Endlos-Scrolling verwenden." } diff --git a/public/translations/el/default.json b/public/translations/el/default.json index ba99249f..e3625b50 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Ενεργοποίηση άπειρου κύλισης", "prev": "Προηγ", "next": "Επόμενο", - "bottom": "Κάτω" + "bottom": "Κάτω", + "enable_infinite_scroll_tip": "Δεν ισχύει για τα πολυπίνακες, που χρησιμοποιούν πάντα την ατελείωτη κύλιση." } diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 63d02cdf..1256080f 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -284,5 +284,6 @@ "media_hosting_all_providers_failed_guidance": "All upload providers failed. Try again or paste a link manually.", "enable_infinite_scroll": "Enable infinite scroll", "prev": "Prev", - "next": "Next" + "next": "Next", + "enable_infinite_scroll_tip": "Does not apply to multiboards, which always use infinite scroll." } diff --git a/public/translations/es/default.json b/public/translations/es/default.json index 8f641a2a..544b8a59 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Activar desplazamiento infinito", "prev": "Anterior", "next": "Siguiente", - "bottom": "Abajo" + "bottom": "Abajo", + "enable_infinite_scroll_tip": "No aplica a los multiboard, que siempre usan scroll infinito." } diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index 047c5fb7..248e94ef 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "فعال کردن اسکرول بی‌نهایت", "prev": "قبلی", "next": "بعدی", - "bottom": "پایین" + "bottom": "پایین", + "enable_infinite_scroll_tip": "برای تابلوهای چندگانه اعمال نمی‌شود، که همیشه از اسکرول بی‌نهایت استفاده می‌کنند." } diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index db9f2984..70a4a4e4 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Ota ääretön vieritys käyttöön", "prev": "Edell.", "next": "Seuraava", - "bottom": "Pohja" + "bottom": "Pohja", + "enable_infinite_scroll_tip": "Ei koske monilautalistauksia, jotka aina käyttävät ääretöntä vieritystä." } diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index ce7cca6c..c64d3556 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Paganahin ang walang hanggang scroll", "prev": "Nakaraan", "next": "Susunod", - "bottom": "Ibaba" + "bottom": "Ibaba", + "enable_infinite_scroll_tip": "Hindi naaangkop sa mga multiboard, na palaging gumagamit ng walang hanggang scroll." } diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index 09e102d4..db2742b2 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Activer le défilement infini", "prev": "Précédent", "next": "Suivant", - "bottom": "Bas" + "bottom": "Bas", + "enable_infinite_scroll_tip": "Ne s'applique pas aux multiboards, qui utilisent toujours le défilement infini." } diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 64944461..b4567f9f 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "הפעל גלילה אינסופית", "prev": "הקודם", "next": "הבא", - "bottom": "תחתית" + "bottom": "תחתית", + "enable_infinite_scroll_tip": "לא חל על לוחות מרובים, שתמיד משתמשים בגלילה אינסופית." } diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index e624a881..fb779c11 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "अनंत स्क्रॉल सक्षम करें", "prev": "पिछला", "next": "अगला", - "bottom": "नीचे" + "bottom": "नीचे", + "enable_infinite_scroll_tip": "मल्टीबोर्ड पर लागू नहीं होता, जो हमेशा अनंत स्क्रॉल का उपयोग करते हैं।" } diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index bb378eaf..588a1934 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Végtelen görgetés engedélyezése", "prev": "Előző", "next": "Következő", - "bottom": "Alul" + "bottom": "Alul", + "enable_infinite_scroll_tip": "A többtáblás nézetekre nem vonatkozik, amelyek mindig végtelen görgetést használnak." } diff --git a/public/translations/id/default.json b/public/translations/id/default.json index 421a6b64..33596916 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Aktifkan scroll tak terbatas", "prev": "Sebelumnya", "next": "Berikutnya", - "bottom": "Bawah" + "bottom": "Bawah", + "enable_infinite_scroll_tip": "Tidak berlaku untuk papan ganda, yang selalu menggunakan scroll tak terbatas." } diff --git a/public/translations/it/default.json b/public/translations/it/default.json index 069b4da2..3fc36ee2 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Abilita scorrimento infinito", "prev": "Prec.", "next": "Avanti", - "bottom": "In basso" + "bottom": "In basso", + "enable_infinite_scroll_tip": "Non si applica ai multiboard, che usano sempre lo scroll infinito." } diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index 9debd480..67cc0c3d 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "無限スクロールを有効にする", "prev": "前へ", "next": "次へ", - "bottom": "下" + "bottom": "下", + "enable_infinite_scroll_tip": "マルチボードには適用されません。マルチボードは常に無限スクロールを使用します。" } diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index ff9da92c..ad0970f0 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "무한 스크롤 활성화", "prev": "이전", "next": "다음", - "bottom": "아래" + "bottom": "아래", + "enable_infinite_scroll_tip": "항상 무한 스크롤을 사용하는 멀티보드에는 적용되지 않습니다." } diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index 8c3f4b94..748c8545 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "अनंत स्क्रोल सक्षम करा", "prev": "मागील", "next": "पुढे", - "bottom": "तळ" + "bottom": "तळ", + "enable_infinite_scroll_tip": "मल्टीबोर्डवर लागू होत नाही, जे नेहमी अंतहीन स्क्रॉल वापरतात." } diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index a21968f2..99698178 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Oneindig scrollen inschakelen", "prev": "Vorige", "next": "Volgende", - "bottom": "Onder" + "bottom": "Onder", + "enable_infinite_scroll_tip": "Geldt niet voor multiboards, die altijd oneindig scrollen gebruiken." } diff --git a/public/translations/no/default.json b/public/translations/no/default.json index c63ae80e..f4cb0956 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Aktiver uendelig rulling", "prev": "Forrige", "next": "Neste", - "bottom": "Bunn" + "bottom": "Bunn", + "enable_infinite_scroll_tip": "Gjelder ikke for multiboard, som alltid bruker uendelig scroll." } diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index 1137fdf5..ed986acb 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Włącz nieskończone przewijanie", "prev": "Poprz.", "next": "Dalej", - "bottom": "Dół" + "bottom": "Dół", + "enable_infinite_scroll_tip": "Nie dotyczy multiboardów, które zawsze używają nieskończonego przewijania." } diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index 81ad8d42..25fd865c 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Ativar rolagem infinita", "prev": "Anterior", "next": "Próximo", - "bottom": "Inferior" + "bottom": "Inferior", + "enable_infinite_scroll_tip": "Não se aplica a multiboard, que sempre usam rolagem infinita." } diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index fda88e44..912affef 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Activează derularea infinită", "prev": "Anterior", "next": "Următorul", - "bottom": "Jos" + "bottom": "Jos", + "enable_infinite_scroll_tip": "Nu se aplică la multiboard-urile, care folosesc întotdeauna defilarea infinită." } diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index 819b66d1..b341f463 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Включить бесконечную прокрутку", "prev": "Пред.", "next": "Далее", - "bottom": "Вниз" + "bottom": "Вниз", + "enable_infinite_scroll_tip": "Не применяется к мультибордам, которые всегда используют бесконечную прокрутку." } diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index 569d2611..4a878d6a 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Aktivizo rrëshqitjen e pafundme", "prev": "Paraprake", "next": "Tjetra", - "bottom": "Poshtë" + "bottom": "Poshtë", + "enable_infinite_scroll_tip": "Nuk aplikohet për multiboard-et, që gjithmonë përdorin rrëshqitje të pafundme." } diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index 6190f4e9..69cd44c6 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Aktivera oändlig scrollning", "prev": "Föreg.", "next": "Nästa", - "bottom": "Botten" + "bottom": "Botten", + "enable_infinite_scroll_tip": "Gäller inte multiboard, som alltid använder oändlig scroll." } diff --git a/public/translations/te/default.json b/public/translations/te/default.json index b0f183be..0a708986 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "అనంత స్క్రోల్ చేతనం చేయండి", "prev": "మునుపటి", "next": "తదుపరి", - "bottom": "క్రింద" + "bottom": "క్రింద", + "enable_infinite_scroll_tip": "మల్టీబోర్డ్‌లకు వర్తించదు, అవి ఎల్లప్పుడూ అనంత స్క్రాల్ ఉపయోగిస్తాయి." } diff --git a/public/translations/th/default.json b/public/translations/th/default.json index 36ecbd90..a2734775 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "เปิดใช้งานการเลื่อนแบบไม่จำกัด", "prev": "ก่อนหน้า", "next": "ถัดไป", - "bottom": "ล่าง" + "bottom": "ล่าง", + "enable_infinite_scroll_tip": "ไม่ใช้กับมัลติบอร์ด ซึ่งใช้การเลื่อนแบบไม่สิ้นสุดเสมอ" } diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index fba3d087..5095ff7e 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Sonsuz kaydırmayı etkinleştir", "prev": "Önceki", "next": "İleri", - "bottom": "Alt" + "bottom": "Alt", + "enable_infinite_scroll_tip": "Her zaman sonsuz kaydırma kullanan çoklu panolar için geçerli değildir." } diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index 5f2b5d31..6f830101 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Увімкнути нескінченну прокрутку", "prev": "Попередн.", "next": "Далі", - "bottom": "Вниз" + "bottom": "Вниз", + "enable_infinite_scroll_tip": "Не застосовується до мультибордів, які завжди використовують нескінченне прокручування." } diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index 704b8512..4a01e5ca 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "لامحدود سکرول فعال کریں", "prev": "پچھلا", "next": "اگلا", - "bottom": "نیچے" + "bottom": "نیچے", + "enable_infinite_scroll_tip": "ملٹی بورڈز پر لاگو نہیں ہوتا، جو ہمیشہ لامحدود سکرول استعمال کرتے ہیں۔" } diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index a3aae13d..017d5870 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "Bật cuộn vô hạn", "prev": "Trước", "next": "Tiếp theo", - "bottom": "Dưới cùng" + "bottom": "Dưới cùng", + "enable_infinite_scroll_tip": "Không áp dụng cho đa bảng, luôn sử dụng cuộn vô hạn." } diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 2b44ad44..008ce27c 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -284,5 +284,6 @@ "enable_infinite_scroll": "启用无限滚动", "prev": "上一页", "next": "下一步", - "bottom": "底部" + "bottom": "底部", + "enable_infinite_scroll_tip": "不适用于多板视图,后者始终使用无限滚动。" } diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx index 8813af7d..33e43501 100644 --- a/src/components/board-buttons/board-buttons.tsx +++ b/src/components/board-buttons/board-buttons.tsx @@ -416,7 +416,9 @@ export const MobileBoardButtons = () => { const { filteredCount, searchText } = useCatalogFiltersStore(); const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); - const showBottomButton = (isInCatalogView || isInPostView || isInPendingPostPage) && !enableInfiniteScroll; + const isMultiboard = isInAllView || isInSubscriptionsView || isInModView; + const effectiveInfiniteScroll = isMultiboard || enableInfiniteScroll; + const showBottomButton = (isInCatalogView || isInPostView || isInPendingPostPage) && !effectiveInfiniteScroll; // Check if we should show the vote button (only for directory boards) const directories = useDirectories(); @@ -539,7 +541,9 @@ export const DesktopBoardButtons = () => { const { filteredCount, searchText } = useCatalogFiltersStore(); const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); - const showBottomButton = (isInCatalogView || isInPostView || isInPendingPostPage) && !enableInfiniteScroll; + const isMultiboard = isInAllView || isInSubscriptionsView || isInModView; + const effectiveInfiniteScroll = isMultiboard || enableInfiniteScroll; + const showBottomButton = (isInCatalogView || isInPostView || isInPendingPostPage) && !effectiveInfiniteScroll; // Check if we should show the vote button (only for directory boards) const directories = useDirectories(); diff --git a/src/components/settings-modal/interface-settings/__tests__/interface-settings.test.tsx b/src/components/settings-modal/interface-settings/__tests__/interface-settings.test.tsx index ecb84f45..0a114a84 100644 --- a/src/components/settings-modal/interface-settings/__tests__/interface-settings.test.tsx +++ b/src/components/settings-modal/interface-settings/__tests__/interface-settings.test.tsx @@ -72,6 +72,11 @@ describe('InterfaceSettings', () => { setItemSpy.mockRestore(); }); + it('renders enable_infinite_scroll_tip under the infinite scroll checkbox', () => { + render(createElement(InterfaceSettings)); + expect(container.textContent).toMatch(/enable_infinite_scroll_tip/i); + }); + it('renders enable_infinite_scroll checkbox unchecked by default', () => { render(createElement(InterfaceSettings)); const label = Array.from(container.querySelectorAll('label')).find((l) => l.textContent?.toLowerCase().includes('enable_infinite_scroll')); diff --git a/src/components/settings-modal/interface-settings/interface-settings.tsx b/src/components/settings-modal/interface-settings/interface-settings.tsx index 4afdc400..a372833b 100644 --- a/src/components/settings-modal/interface-settings/interface-settings.tsx +++ b/src/components/settings-modal/interface-settings/interface-settings.tsx @@ -153,6 +153,7 @@ const InterfaceSettings = () => { setEnableInfiniteScroll(e.target.checked)} /> {capitalize(t('enable_infinite_scroll'))} +
{capitalize(t('enable_infinite_scroll_tip'))}
); diff --git a/src/lib/utils/__tests__/route-utils.test.ts b/src/lib/utils/__tests__/route-utils.test.ts new file mode 100644 index 00000000..6a01c858 --- /dev/null +++ b/src/lib/utils/__tests__/route-utils.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest'; +import { normalizeMultiboardFeedPath } from '../route-utils'; + +describe('normalizeMultiboardFeedPath', () => { + it('normalizes /all/3 -> /all', () => { + expect(normalizeMultiboardFeedPath('/all/3')).toBe('/all'); + }); + + it('normalizes /all/1w/3 -> /all/1w', () => { + expect(normalizeMultiboardFeedPath('/all/1w/3')).toBe('/all/1w'); + }); + + it('normalizes /subs/2/settings -> /subs/settings', () => { + expect(normalizeMultiboardFeedPath('/subs/2/settings')).toBe('/subs/settings'); + }); + + it('normalizes /mod/catalog/4 -> /mod/catalog', () => { + expect(normalizeMultiboardFeedPath('/mod/catalog/4')).toBe('/mod/catalog'); + }); + + it('leaves non-multiboard paths unchanged', () => { + expect(normalizeMultiboardFeedPath('/biz')).toBe('/biz'); + expect(normalizeMultiboardFeedPath('/biz/3')).toBe('/biz/3'); + expect(normalizeMultiboardFeedPath('/biz/1w/2')).toBe('/biz/1w/2'); + expect(normalizeMultiboardFeedPath('/biz/catalog/4')).toBe('/biz/catalog/4'); + expect(normalizeMultiboardFeedPath('/pending/0')).toBe('/pending/0'); + expect(normalizeMultiboardFeedPath('/')).toBe('/'); + }); +}); diff --git a/src/lib/utils/route-utils.ts b/src/lib/utils/route-utils.ts index 090b224a..ed6c4b53 100644 --- a/src/lib/utils/route-utils.ts +++ b/src/lib/utils/route-utils.ts @@ -150,6 +150,43 @@ export const BOARD_PAGE_REGEX = /^([1-9]|10)$/; export const isBoardFeedPageNumber = (segment: string): boolean => BOARD_PAGE_REGEX.test(segment); +/** Internal: check if segment is a multiboard root (all, subs, mod) */ +function isMultiboardRoot(segment: string): boolean { + return segment === 'all' || segment === 'subs' || segment === 'mod'; +} + +/** Internal: check if pathname is a multiboard feed path (starts with /all, /subs, or /mod) */ +function isMultiboardFeedPath(pathname: string): boolean { + const trimmed = pathname.replace(/\/$/, ''); + const segments = trimmed.split('/').filter(Boolean); + return segments.length > 0 && isMultiboardRoot(segments[0]); +} + +/** + * Normalize multiboard feed paths by removing trailing page-number segments (1–10), + * while preserving /settings and valid time-filter segments. + * Non-multiboard paths are returned unchanged. + */ +export const normalizeMultiboardFeedPath = (pathname: string): string => { + let path = pathname.replace(/\/$/, ''); + if (!isMultiboardFeedPath(path)) { + return pathname; + } + + const hasSettings = path.endsWith('/settings'); + if (hasSettings) { + path = path.replace(/\/settings$/, ''); + } + + const segments = path.split('/').filter(Boolean); + if (segments.length > 1 && isBoardFeedPageNumber(segments[segments.length - 1])) { + const newPath = '/' + segments.slice(0, -1).join('/'); + return hasSettings ? newPath + '/settings' : newPath; + } + + return hasSettings ? path + '/settings' : path; +}; + /** Strip trailing page number (1–10) from path for feed cache key */ export const stripPageFromFeedPath = (path: string): string => { const segments = path.split('/').filter(Boolean); diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index 14a9198d..fbc50d1f 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -16,7 +16,7 @@ import useSortingStore from '../../stores/use-sorting-store'; import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store'; import { useBoardFeedPageSize } from '../../hooks/use-board-feed-page-size'; import { getPageSlice } from '../../lib/utils/board-feed-pagination'; -import { getPageFromFeedPath, getSubplebbitAddress, isDirectoryBoard, stripPageFromFeedPath } from '../../lib/utils/route-utils'; +import { getPageFromFeedPath, getSubplebbitAddress, isDirectoryBoard, normalizeMultiboardFeedPath, stripPageFromFeedPath } from '../../lib/utils/route-utils'; import ErrorDisplay from '../../components/error-display/error-display'; import LoadingEllipsis from '../../components/loading-ellipsis'; import BoardPagination from '../../components/board-pagination'; @@ -209,6 +209,8 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t const timeFilterSeconds = timeFilterNameFromCache ? timeFilterNameToSeconds(timeFilterNameFromCache) : timeFilterSecondsFromHook; const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); + const isForcedInfiniteScroll = isInAllView || isInSubscriptionsView || isInModView; + const effectiveInfiniteScroll = enableInfiniteScroll || isForcedInfiniteScroll; const community = useDirectoryByAddress(isInAllView || isInSubscriptionsView || isInModView ? undefined : subplebbitAddress); const { guiPostsPerPage, maxGuiPages, paginationFeedPostsPerPage, infiniteFeedPostsPerPage } = useBoardFeedPageSize(community); @@ -216,13 +218,13 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t () => ({ subplebbitAddresses, sortType, - postsPerPage: enableInfiniteScroll ? infiniteFeedPostsPerPage : paginationFeedPostsPerPage, + postsPerPage: effectiveInfiniteScroll ? infiniteFeedPostsPerPage : paginationFeedPostsPerPage, ...(isInAllView || isInSubscriptionsView || isInModView ? { newerThan: timeFilterSeconds } : {}), }), [ subplebbitAddresses, sortType, - enableInfiniteScroll, + effectiveInfiniteScroll, infiniteFeedPostsPerPage, paginationFeedPostsPerPage, isInAllView, @@ -235,7 +237,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions); const { accountComments } = useAccountComments(); - const feedContextKey = `${isInAllView ? 'all' : isInSubscriptionsView ? 'subs' : isInModView ? 'mod' : (subplebbitAddress ?? 'board')}-${sortType}-${timeFilterSeconds}-${viewType ?? 'board'}-${enableInfiniteScroll}`; + const feedContextKey = `${isInAllView ? 'all' : isInSubscriptionsView ? 'subs' : isInModView ? 'mod' : (subplebbitAddress ?? 'board')}-${sortType}-${timeFilterSeconds}-${viewType ?? 'board'}-${effectiveInfiniteScroll}`; const pathWithoutSettings = location.pathname.replace(/\/settings$/, ''); const currentPage = getPageFromFeedPath(pathWithoutSettings); const paginationBasePath = stripPageFromFeedPath(pathWithoutSettings); @@ -279,30 +281,40 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t }, [feed, filteredComments]); const cappedFeed = useMemo( - () => (enableInfiniteScroll ? combinedFeed : combinedFeed.slice(0, guiPostsPerPage * maxGuiPages)), - [enableInfiniteScroll, combinedFeed, guiPostsPerPage, maxGuiPages], + () => (effectiveInfiniteScroll ? combinedFeed : combinedFeed.slice(0, guiPostsPerPage * maxGuiPages)), + [effectiveInfiniteScroll, combinedFeed, guiPostsPerPage, maxGuiPages], ); const totalPages = useMemo(() => Math.min(maxGuiPages, Math.ceil(cappedFeed.length / guiPostsPerPage) || 1), [cappedFeed.length, guiPostsPerPage, maxGuiPages]); const currentPageFeed = useMemo( - () => (enableInfiniteScroll ? [] : getPageSlice(cappedFeed, currentPage, guiPostsPerPage, maxGuiPages)), - [enableInfiniteScroll, cappedFeed, currentPage, guiPostsPerPage, maxGuiPages], + () => (effectiveInfiniteScroll ? [] : getPageSlice(cappedFeed, currentPage, guiPostsPerPage, maxGuiPages)), + [effectiveInfiniteScroll, cappedFeed, currentPage, guiPostsPerPage, maxGuiPages], ); const navigate = useNavigate(); + + // Redirect multiboard paths with page-number segments to normalized path (infinite-scroll only) useEffect(() => { - if (!enableInfiniteScroll && currentPage > totalPages && totalPages > 0) { + if (!isForcedInfiniteScroll) return; + const normalized = normalizeMultiboardFeedPath(location.pathname); + if (normalized !== location.pathname) { + navigate(normalized, { replace: true }); + } + }, [isForcedInfiniteScroll, location.pathname, navigate]); + + useEffect(() => { + if (!effectiveInfiniteScroll && currentPage > totalPages && totalPages > 0) { const targetPage = totalPages; const targetPath = targetPage === 1 ? paginationBasePath : `${paginationBasePath}/${targetPage}`; - navigate(targetPage === 1 ? paginationBasePath : `${paginationBasePath}/${targetPage}`, { replace: true }); + navigate(targetPath, { replace: true }); } - }, [enableInfiniteScroll, currentPage, totalPages, paginationBasePath, navigate]); + }, [effectiveInfiniteScroll, currentPage, totalPages, paginationBasePath, navigate]); // Scroll to top instantly when page changes in pagination mode useEffect(() => { - if (!enableInfiniteScroll) { + if (!effectiveInfiniteScroll) { window.scrollTo({ top: 0, left: 0, behavior: 'instant' }); } - }, [enableInfiniteScroll, currentPage]); + }, [effectiveInfiniteScroll, currentPage]); useEffect(() => { if (filteredComments.length > 0 && !resetTriggeredRef.current) { @@ -383,7 +395,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t subplebbitState={subplebbitState} subscriptionsLength={subscriptions?.length || 0} accountSubplebbitAddressesLength={accountSubplebbitAddresses?.length || 0} - showLoadingEllipsis={enableInfiniteScroll || combinedFeed.length === 0} + showLoadingEllipsis={effectiveInfiniteScroll || combinedFeed.length === 0} /> ), }), @@ -406,7 +418,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t subplebbitState, subscriptions?.length, accountSubplebbitAddresses?.length, - enableInfiniteScroll, + effectiveInfiniteScroll, combinedFeed.length, ], ); @@ -474,7 +486,7 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, t )} {/* Infinite mode: Virtuoso when hasMore, else plain list */} - {enableInfiniteScroll ? ( + {effectiveInfiniteScroll ? ( hasMore ? ( void; isInAllView: boolean; isInSubscriptionsView: boolean; + isInModView: boolean; showMorePostsSuggestion: boolean; weeklyFeedLength: number; monthlyFeedLength: number; @@ -56,6 +57,7 @@ const CatalogFooter = ({ onNewerPostsClick, isInAllView, isInSubscriptionsView, + isInModView, showMorePostsSuggestion, weeklyFeedLength, monthlyFeedLength, @@ -91,7 +93,7 @@ const CatalogFooter = ({ /> ) : ( - (isInAllView || isInSubscriptionsView) && + (isInAllView || isInSubscriptionsView || isInModView) && showMorePostsSuggestion && (monthlyFeedLength > feedLength || yearlyFeedLength > monthlyFeedLength) && (weeklyFeedLength > feedLength ? ( @@ -100,7 +102,11 @@ const CatalogFooter = ({ i18nKey='more_threads_last_week' values={{ currentTimeFilterName, count: feedLength }} components={{ - 1: , + 1: ( + + ), }} /> @@ -110,7 +116,11 @@ const CatalogFooter = ({ i18nKey='more_threads_last_month' values={{ currentTimeFilterName, count: feedLength }} components={{ - 1: , + 1: ( + + ), }} /> @@ -120,7 +130,11 @@ const CatalogFooter = ({ i18nKey='more_threads_last_year' values={{ currentTimeFilterName, count: feedLength }} components={{ - 1: , + 1: ( + + ), }} /> @@ -282,10 +296,16 @@ export interface CatalogProps { const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, timeFilterNameFromCache, isVisible = true }: CatalogProps) => { const { t } = useTranslation(); const location = useLocation(); + const navigate = useNavigate(); const params = useParams(); const isInAllView = viewType ? viewType === 'all' : false; const isInSubscriptionsView = viewType ? viewType === 'subs' : false; + const isInModView = viewType ? viewType === 'mod' : false; + + const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); + const isForcedInfiniteScroll = isInAllView || isInSubscriptionsView || isInModView; + const effectiveInfiniteScroll = enableInfiniteScroll || isForcedInfiniteScroll; const directories = useDirectories(); const resolvedAddressFromUrl = useResolvedSubplebbitAddress(); @@ -320,10 +340,18 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, const columnCount = Math.floor(useWindowWidth() / columnWidth); const postsPerPage = columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25; - const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll); - const community = useDirectoryByAddress(isInAllView || isInSubscriptionsView ? undefined : subplebbitAddress); + const community = useDirectoryByAddress(isInAllView || isInSubscriptionsView || isInModView ? undefined : subplebbitAddress); const { guiPostsPerPage: boardPostsPerPage, maxGuiPages, paginationFeedPostsPerPage, infiniteFeedPostsPerPage } = useBoardFeedPageSize(community); + // Canonical redirect for multiboard catalog paths with numeric page segment (e.g. /all/catalog/1w/5 -> /all/catalog/1w) + useEffect(() => { + if (!(isInAllView || isInSubscriptionsView || isInModView)) return; + const canonical = normalizeMultiboardFeedPath(location.pathname); + if (location.pathname !== canonical) { + navigate(canonical, { replace: true }); + } + }, [isInAllView, isInSubscriptionsView, isInModView, location.pathname, navigate]); + const { timeFilterSeconds: timeFilterSecondsFromHook, timeFilterName: timeFilterNameFromHook } = useTimeFilter(); const { sortType } = useSortingStore(); const timeFilterName = timeFilterNameFromCache || timeFilterNameFromHook; @@ -344,7 +372,13 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, const feedOptions = useMemo(() => { const catalogPostsPerPage = - isInAllView || isInSubscriptionsView ? (enableInfiniteScroll ? 10 : 100) : enableInfiniteScroll ? infiniteFeedPostsPerPage : paginationFeedPostsPerPage; + isInAllView || isInSubscriptionsView || isInModView + ? effectiveInfiniteScroll + ? 10 + : 100 + : effectiveInfiniteScroll + ? infiniteFeedPostsPerPage + : paginationFeedPostsPerPage; const options: any = { subplebbitAddresses, @@ -353,7 +387,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, filter: createCombinedFilter(filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch), }; - if (isInAllView || isInSubscriptionsView) { + if (isInAllView || isInSubscriptionsView || isInModView) { options.newerThan = timeFilterSeconds; } @@ -363,7 +397,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, sortType, isInAllView, isInSubscriptionsView, - enableInfiniteScroll, + isInModView, + effectiveInfiniteScroll, infiniteFeedPostsPerPage, paginationFeedPostsPerPage, timeFilterSeconds, @@ -420,8 +455,8 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, }, [feed, filteredComments]); const cappedFeed = useMemo( - () => (enableInfiniteScroll ? combinedFeed : combinedFeed.slice(0, boardPostsPerPage * maxGuiPages)), - [enableInfiniteScroll, combinedFeed, boardPostsPerPage, maxGuiPages], + () => (effectiveInfiniteScroll ? combinedFeed : combinedFeed.slice(0, boardPostsPerPage * maxGuiPages)), + [effectiveInfiniteScroll, combinedFeed, boardPostsPerPage, maxGuiPages], ); useEffect(() => { @@ -499,13 +534,14 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, onNewerPostsClick={handleNewerPostsButtonClick} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} + isInModView={isInModView} showMorePostsSuggestion={showMorePostsSuggestion} weeklyFeedLength={weeklyFeedLength} monthlyFeedLength={monthlyFeedLength} yearlyFeedLength={yearlyFeedLength} boardPath={boardPath} currentTimeFilterName={currentTimeFilterName} - showLoadingEllipsis={enableInfiniteScroll} + showLoadingEllipsis={effectiveInfiniteScroll} /> ), }), @@ -518,13 +554,14 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, handleNewerPostsButtonClick, isInAllView, isInSubscriptionsView, + isInModView, showMorePostsSuggestion, weeklyFeedLength, monthlyFeedLength, yearlyFeedLength, boardPath, currentTimeFilterName, - enableInfiniteScroll, + effectiveInfiniteScroll, ], ); @@ -650,7 +687,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
{processedFeed?.length !== 0 ? ( <> - {enableInfiniteScroll ? ( + {effectiveInfiniteScroll ? ( hasMore ? (