Merge pull request #723 from plebbit/development

Development
This commit is contained in:
plebeius
2025-06-04 16:40:51 +02:00
committed by GitHub
50 changed files with 448 additions and 102 deletions
-1
View File
@@ -23,7 +23,6 @@
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron-context-menu": "3.3.0", "electron-context-menu": "3.3.0",
"electron-is-dev": "2.0.0", "electron-is-dev": "2.0.0",
"ext-name": "5.0.0",
"fs-extra": "11.3.0", "fs-extra": "11.3.0",
"http-proxy": "1.18.1", "http-proxy": "1.18.1",
"i18next": "23.5.1", "i18next": "23.5.1",
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "نتائج البحث عن", "search_results_for": "نتائج البحث عن",
"help": "مساعدة", "help": "مساعدة",
"filter_and_highlights_help": "مساعدة الفلاتر والإبرازات", "filter_and_highlights_help": "مساعدة الفلاتر والإبرازات",
"filter_and_highlights": "التصفية والتسليط الضوء" "filter_and_highlights": "التصفية والتسليط الضوء",
"boards_you_moderate": "المنتديات التي تقوم بالإشراف عليها"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "অনুসন্ধান ফলাফল", "search_results_for": "অনুসন্ধান ফলাফল",
"help": "সাহায্য", "help": "সাহায্য",
"filter_and_highlights_help": "ফিল্টার এবং হাইলাইট সাহায্য", "filter_and_highlights_help": "ফিল্টার এবং হাইলাইট সাহায্য",
"filter_and_highlights": "ফিল্টার এবং হাইলাইট" "filter_and_highlights": "ফিল্টার এবং হাইলাইট",
"boards_you_moderate": "আপনি যে বোর্ডগুলি পরিচালনা করেন"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Výsledky hledání pro", "search_results_for": "Výsledky hledání pro",
"help": "Pomoc", "help": "Pomoc",
"filter_and_highlights_help": "Pomoc s filtry a zvýrazněním", "filter_and_highlights_help": "Pomoc s filtry a zvýrazněním",
"filter_and_highlights": "Filtry a zvýraznění" "filter_and_highlights": "Filtry a zvýraznění",
"boards_you_moderate": "Fóra, která moderujete"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Søgeresultater for", "search_results_for": "Søgeresultater for",
"help": "Hjælp", "help": "Hjælp",
"filter_and_highlights_help": "Filtre og fremhævelser Hjælp", "filter_and_highlights_help": "Filtre og fremhævelser Hjælp",
"filter_and_highlights": "Filtre og fremhævninger" "filter_and_highlights": "Filtre og fremhævninger",
"boards_you_moderate": "Boards du modererer"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Suchergebnisse für", "search_results_for": "Suchergebnisse für",
"help": "Hilfe", "help": "Hilfe",
"filter_and_highlights_help": "Filter- und Markierungs-Hilfe", "filter_and_highlights_help": "Filter- und Markierungs-Hilfe",
"filter_and_highlights": "Filter und Hervorhebungen" "filter_and_highlights": "Filter und Hervorhebungen",
"boards_you_moderate": "Boards, die Sie moderieren"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Αποτελέσματα αναζήτησης για", "search_results_for": "Αποτελέσματα αναζήτησης για",
"help": "Βοήθεια", "help": "Βοήθεια",
"filter_and_highlights_help": "Βοήθεια φίλτρων και επισήμανσης", "filter_and_highlights_help": "Βοήθεια φίλτρων και επισήμανσης",
"filter_and_highlights": "Φίλτρα και Επισημάνσεις" "filter_and_highlights": "Φίλτρα και Επισημάνσεις",
"boards_you_moderate": "Boards που διαχειρίζεστε"
} }
+3 -1
View File
@@ -141,6 +141,7 @@
"user_deleted_this_post": "User deleted this post.", "user_deleted_this_post": "User deleted this post.",
"reason": "reason", "reason": "reason",
"search": "search", "search": "search",
"search_ops_placeholder": "Search OPs...",
"ban_user_for": "Ban user for <1></1> days", "ban_user_for": "Ban user for <1></1> days",
"optional": "optional", "optional": "optional",
"close_thread": "close thread", "close_thread": "close thread",
@@ -294,5 +295,6 @@
"search_results_for": "Search results for", "search_results_for": "Search results for",
"help": "Help", "help": "Help",
"filter_and_highlights_help": "Filters & Highlights Help", "filter_and_highlights_help": "Filters & Highlights Help",
"filter_and_highlights": "Filters & Highlights" "filter_and_highlights": "Filters & Highlights",
"boards_you_moderate": "boards you moderate"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Resultados de búsqueda para", "search_results_for": "Resultados de búsqueda para",
"help": "Ayuda", "help": "Ayuda",
"filter_and_highlights_help": "Ayuda de filtros y resaltes", "filter_and_highlights_help": "Ayuda de filtros y resaltes",
"filter_and_highlights": "Filtros y resaltados" "filter_and_highlights": "Filtros y resaltados",
"boards_you_moderate": "tableros que moderas"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "نتایج جستجو برای", "search_results_for": "نتایج جستجو برای",
"help": "کمک", "help": "کمک",
"filter_and_highlights_help": "کمک فیلترها و برجسته‌ها", "filter_and_highlights_help": "کمک فیلترها و برجسته‌ها",
"filter_and_highlights": "فیلترها و برجسته‌ها" "filter_and_highlights": "فیلترها و برجسته‌ها",
"boards_you_moderate": "تابلوهایی که مدیریت می‌کنید"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Hakutulokset hakusanalle", "search_results_for": "Hakutulokset hakusanalle",
"help": "Apua", "help": "Apua",
"filter_and_highlights_help": "Suodattimet ja korostukset -apu", "filter_and_highlights_help": "Suodattimet ja korostukset -apu",
"filter_and_highlights": "Suodattimet ja korostukset" "filter_and_highlights": "Suodattimet ja korostukset",
"boards_you_moderate": "lautakunnat, joita valvot"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Mga resulta ng paghahanap para sa", "search_results_for": "Mga resulta ng paghahanap para sa",
"help": "Tulong", "help": "Tulong",
"filter_and_highlights_help": "Tulong sa mga filter at highlight", "filter_and_highlights_help": "Tulong sa mga filter at highlight",
"filter_and_highlights": "Mga filter at highlight" "filter_and_highlights": "Mga filter at highlight",
"boards_you_moderate": "mga board na iyong minomoderate"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Résultats de recherche pour", "search_results_for": "Résultats de recherche pour",
"help": "Aide", "help": "Aide",
"filter_and_highlights_help": "Aide sur les filtres et surlignages", "filter_and_highlights_help": "Aide sur les filtres et surlignages",
"filter_and_highlights": "Filtres et surlignages" "filter_and_highlights": "Filtres et surlignages",
"boards_you_moderate": "boards que vous modérez"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "תוצאות חיפוש עבור", "search_results_for": "תוצאות חיפוש עבור",
"help": "עזרה", "help": "עזרה",
"filter_and_highlights_help": "עזרה במסננים והדגשות", "filter_and_highlights_help": "עזרה במסננים והדגשות",
"filter_and_highlights": "מסננים והדגשות" "filter_and_highlights": "מסננים והדגשות",
"boards_you_moderate": "לוחות שאתה מפקח עליהם"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "के लिए खोज परिणाम", "search_results_for": "के लिए खोज परिणाम",
"help": "मदद", "help": "मदद",
"filter_and_highlights_help": "फ़िल्टर और हाइलाइट्स सहायता", "filter_and_highlights_help": "फ़िल्टर और हाइलाइट्स सहायता",
"filter_and_highlights": "फ़िल्टर और हाइलाइट्स" "filter_and_highlights": "फ़िल्टर और हाइलाइट्स",
"boards_you_moderate": "बोर्ड जो आप मॉडरेट करते हैं"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Keresési eredmények", "search_results_for": "Keresési eredmények",
"help": "Segítség", "help": "Segítség",
"filter_and_highlights_help": "Szűrők és kiemelések súgó", "filter_and_highlights_help": "Szűrők és kiemelések súgó",
"filter_and_highlights": "Szűrők és kiemelések" "filter_and_highlights": "Szűrők és kiemelések",
"boards_you_moderate": "boardok, amelyeket moderálsz"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Hasil pencarian untuk", "search_results_for": "Hasil pencarian untuk",
"help": "Bantuan", "help": "Bantuan",
"filter_and_highlights_help": "Bantuan Filter dan Sorotan", "filter_and_highlights_help": "Bantuan Filter dan Sorotan",
"filter_and_highlights": "Filter dan Sorotan" "filter_and_highlights": "Filter dan Sorotan",
"boards_you_moderate": "board yang Anda moderasi"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Risultati di ricerca per", "search_results_for": "Risultati di ricerca per",
"help": "Aiuto", "help": "Aiuto",
"filter_and_highlights_help": "Aiuto su Filtri e Evidenziazioni", "filter_and_highlights_help": "Aiuto su Filtri e Evidenziazioni",
"filter_and_highlights": "Filtri e Evidenziazioni" "filter_and_highlights": "Filtri e Evidenziazioni",
"boards_you_moderate": "board che moderi"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "検索結果", "search_results_for": "検索結果",
"help": "ヘルプ", "help": "ヘルプ",
"filter_and_highlights_help": "フィルターとハイライトのヘルプ", "filter_and_highlights_help": "フィルターとハイライトのヘルプ",
"filter_and_highlights": "フィルターとハイライト" "filter_and_highlights": "フィルターとハイライト",
"boards_you_moderate": "あなたが管理するボード"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "검색 결과", "search_results_for": "검색 결과",
"help": "도움말", "help": "도움말",
"filter_and_highlights_help": "필터 및 하이라이트 도움말", "filter_and_highlights_help": "필터 및 하이라이트 도움말",
"filter_and_highlights": "필터 및 하이라이트" "filter_and_highlights": "필터 및 하이라이트",
"boards_you_moderate": "당신이 관리하는 게시판"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "सर्च परिणाम", "search_results_for": "सर्च परिणाम",
"help": "मदत", "help": "मदत",
"filter_and_highlights_help": "फिल्टर आणि हायलाइट्स सहाय्य", "filter_and_highlights_help": "फिल्टर आणि हायलाइट्स सहाय्य",
"filter_and_highlights": "फिल्टर आणि हायलाइट्स" "filter_and_highlights": "फिल्टर आणि हायलाइट्स",
"boards_you_moderate": "तुम्ही ज्यांचे नियंत्रण करता अशा बोर्ड्स"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Zoekresultaten voor", "search_results_for": "Zoekresultaten voor",
"help": "Help", "help": "Help",
"filter_and_highlights_help": "Filters en Markeringen Help", "filter_and_highlights_help": "Filters en Markeringen Help",
"filter_and_highlights": "Filters en markeringen" "filter_and_highlights": "Filters en markeringen",
"boards_you_moderate": "boards die je beheert"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Søkeresultater for", "search_results_for": "Søkeresultater for",
"help": "Hjelp", "help": "Hjelp",
"filter_and_highlights_help": "Filtre og fremhevinger Hjelp", "filter_and_highlights_help": "Filtre og fremhevinger Hjelp",
"filter_and_highlights": "Filtre og uthevinger" "filter_and_highlights": "Filtre og uthevinger",
"boards_you_moderate": "boards du modererer"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Wyniki wyszukiwania dla", "search_results_for": "Wyniki wyszukiwania dla",
"help": "Pomoc", "help": "Pomoc",
"filter_and_highlights_help": "Pomoc dotycząca filtrów i wyróżnień", "filter_and_highlights_help": "Pomoc dotycząca filtrów i wyróżnień",
"filter_and_highlights": "Filtry i wyróżnienia" "filter_and_highlights": "Filtry i wyróżnienia",
"boards_you_moderate": "boards, które moderujesz"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Resultados de pesquisa para", "search_results_for": "Resultados de pesquisa para",
"help": "Ajuda", "help": "Ajuda",
"filter_and_highlights_help": "Ajuda de Filtros e Realces", "filter_and_highlights_help": "Ajuda de Filtros e Realces",
"filter_and_highlights": "Filtros e Destaques" "filter_and_highlights": "Filtros e Destaques",
"boards_you_moderate": "boards que você modera"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Rezultatele căutării pentru", "search_results_for": "Rezultatele căutării pentru",
"help": "Ajutor", "help": "Ajutor",
"filter_and_highlights_help": "Ajutor pentru filtre și evidențieri", "filter_and_highlights_help": "Ajutor pentru filtre și evidențieri",
"filter_and_highlights": "Filtre și evidențieri" "filter_and_highlights": "Filtre și evidențieri",
"boards_you_moderate": "boards pe care le moderezi"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Результаты поиска для", "search_results_for": "Результаты поиска для",
"help": "Помощь", "help": "Помощь",
"filter_and_highlights_help": "Помощь с фильтрами и выделениями", "filter_and_highlights_help": "Помощь с фильтрами и выделениями",
"filter_and_highlights": "Фильтры и выделения" "filter_and_highlights": "Фильтры и выделения",
"boards_you_moderate": "boards, которые вы модерируете"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Rezultatet e kërkimit për", "search_results_for": "Rezultatet e kërkimit për",
"help": "Ndihmë", "help": "Ndihmë",
"filter_and_highlights_help": "Ndihma për Filtro & Thekso", "filter_and_highlights_help": "Ndihma për Filtro & Thekso",
"filter_and_highlights": "Filtra dhe thekson" "filter_and_highlights": "Filtra dhe thekson",
"boards_you_moderate": "boards që i moderoni"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Sökresultat för", "search_results_for": "Sökresultat för",
"help": "Hjälp", "help": "Hjälp",
"filter_and_highlights_help": "Filter och markeringar Hjälp", "filter_and_highlights_help": "Filter och markeringar Hjälp",
"filter_and_highlights": "Filter och markeringar" "filter_and_highlights": "Filter och markeringar",
"boards_you_moderate": "boards du modererar"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "శోధన ఫలితాలు", "search_results_for": "శోధన ఫలితాలు",
"help": "సహాయం", "help": "సహాయం",
"filter_and_highlights_help": "ఫిల్టర్లు మరియు హైలైట్ సహాయం", "filter_and_highlights_help": "ఫిల్టర్లు మరియు హైలైట్ సహాయం",
"filter_and_highlights": "ఫిల్టర్లు మరియు హైలైట్స్" "filter_and_highlights": "ఫిల్టర్లు మరియు హైలైట్స్",
"boards_you_moderate": "మీరు మోడరేట్ చేసే బోర్డులు"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "ผลการค้นหาสำหรับ", "search_results_for": "ผลการค้นหาสำหรับ",
"help": "ช่วยเหลือ", "help": "ช่วยเหลือ",
"filter_and_highlights_help": "ตัวกรองและการเน้นความช่วยเหลือ", "filter_and_highlights_help": "ตัวกรองและการเน้นความช่วยเหลือ",
"filter_and_highlights": "ตัวกรองและไฮไลท์" "filter_and_highlights": "ตัวกรองและไฮไลท์",
"boards_you_moderate": "บอร์ดที่คุณดูแล"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Arama sonuçları için", "search_results_for": "Arama sonuçları için",
"help": "Yardım", "help": "Yardım",
"filter_and_highlights_help": "Filtreler ve Vurgular Yardımı", "filter_and_highlights_help": "Filtreler ve Vurgular Yardımı",
"filter_and_highlights": "Filtreler ve Vurgular" "filter_and_highlights": "Filtreler ve Vurgular",
"boards_you_moderate": "modere ettiğiniz panolar"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Результати пошуку для", "search_results_for": "Результати пошуку для",
"help": "Допомога", "help": "Допомога",
"filter_and_highlights_help": "Допомога з фільтрами та підсвічуваннями", "filter_and_highlights_help": "Допомога з фільтрами та підсвічуваннями",
"filter_and_highlights": "Фільтри та виділення" "filter_and_highlights": "Фільтри та виділення",
"boards_you_moderate": "boards, які ви модеруватимете"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "تلاش کے نتائج", "search_results_for": "تلاش کے نتائج",
"help": "مدد", "help": "مدد",
"filter_and_highlights_help": "فلٹرز اور ہائی لائٹس کی مدد", "filter_and_highlights_help": "فلٹرز اور ہائی لائٹس کی مدد",
"filter_and_highlights": "فلٹرز اور ہائلाइटس" "filter_and_highlights": "فلٹرز اور ہائلाइटس",
"boards_you_moderate": "وہ بورڈز جن کی آپ نگرانی کرتے ہیں"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "Kết quả tìm kiếm cho", "search_results_for": "Kết quả tìm kiếm cho",
"help": "Trợ giúp", "help": "Trợ giúp",
"filter_and_highlights_help": "Trợ giúp bộ lọc và đánh dấu", "filter_and_highlights_help": "Trợ giúp bộ lọc và đánh dấu",
"filter_and_highlights": "Bộ lọc và Nổi bật" "filter_and_highlights": "Bộ lọc và Nổi bật",
"boards_you_moderate": "boards bạn điều hành"
} }
+2 -1
View File
@@ -294,5 +294,6 @@
"search_results_for": "搜索结果", "search_results_for": "搜索结果",
"help": "帮助", "help": "帮助",
"filter_and_highlights_help": "过滤器和高亮帮助", "filter_and_highlights_help": "过滤器和高亮帮助",
"filter_and_highlights": "筛选器与高亮" "filter_and_highlights": "筛选器与高亮",
"boards_you_moderate": "您管理的板块"
} }
+1 -1
View File
@@ -17,7 +17,7 @@ let releaseChangelog =
// format // format
releaseChangelog = releaseChangelog.trim().replace(/\n\n+/g, '\n\n') releaseChangelog = releaseChangelog.trim().replace(/\n\n+/g, '\n\n')
const releaseBody = `This version fixes a bug where the android app would launch in fullscreen mode. const releaseBody = `This version adds minor features and fixes some bugs.
- Web app: https://plebchan.app - Web app: https://plebchan.app
- Decentralized web app: https://plebchan.eth (only works on [Brave Browser](https://brave.com/) or via [IPFS Companion](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites)) - Decentralized web app: https://plebchan.eth (only works on [Brave Browser](https://brave.com/) or via [IPFS Companion](https://docs.ipfs.tech/install/ipfs-companion/#prerequisites))
+5 -4
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
import { Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom'; import { Outlet, Route, Routes, useLocation, useParams } from 'react-router-dom';
import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import { initSnow, removeSnow } from './lib/snow'; import { initSnow, removeSnow } from './lib/snow';
import { isAllView, isSubscriptionsView } from './lib/utils/view-utils'; import { isAllView, isModView, isSubscriptionsView } from './lib/utils/view-utils';
import useReplyModalStore from './stores/use-reply-modal-store'; import useReplyModalStore from './stores/use-reply-modal-store';
import useSpecialThemeStore from './stores/use-special-theme-store'; import useSpecialThemeStore from './stores/use-special-theme-store';
import useIsMobile from './hooks/use-is-mobile'; import useIsMobile from './hooks/use-is-mobile';
@@ -30,6 +30,7 @@ const BoardLayout = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isInAllView = isAllView(location.pathname); const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined }); const pendingPost = useAccountComment({ commentIndex: accountCommentIndex ? parseInt(accountCommentIndex) : undefined });
// Christmas theme // Christmas theme
@@ -53,17 +54,17 @@ const BoardLayout = () => {
<TopBar /> <TopBar />
<BoardHeader /> <BoardHeader />
{isMobile {isMobile
? (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( ? (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
<> <>
<PostForm key={key} /> <PostForm key={key} />
<hr /> <hr />
<MobileBoardButtons /> <MobileBoardButtons />
</> </>
) )
: (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( : (subplebbitAddress || isInAllView || isInModView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && (
<> <>
<PostForm key={key} /> <PostForm key={key} />
{!(isInAllView || isInSubscriptionsView) && <SubplebbitStats />} {!(isInAllView || isInSubscriptionsView || isInModView) && <SubplebbitStats />}
<DesktopBoardButtons /> <DesktopBoardButtons />
</> </>
)} )}
@@ -88,6 +88,27 @@
margin-bottom: 15px; margin-bottom: 15px;
} }
.searchOPsInput {
height: 16px;
line-height: 16px;
margin-left: 2px;
padding: 0 2px;
width: 120px;
margin: 0px;
margin-right: 4px;
padding: 0 2px;
border: var(--post-form-field-input-border, revert);
background-color: var(--post-form-field-input-background-color, revert);
color: var(--post-form-field-input-color);
outline: none;
font-family: arial, helvetica, sans-serif;
font-size: 10pt;
}
.searchOPsInput:focus {
border: var(--post-form-field-input-focus-border, revert);
}
@media (max-width: 640px) { @media (max-width: 640px) {
.desktopBoardButtons { .desktopBoardButtons {
display: none; display: none;
+69 -16
View File
@@ -3,7 +3,7 @@ import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks'; import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isCatalogView, isDescriptionView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useCatalogStyleStore from '../../stores/use-catalog-style-store';
import useFeedResetStore from '../../stores/use-feed-reset-store'; import useFeedResetStore from '../../stores/use-feed-reset-store';
@@ -22,10 +22,11 @@ interface BoardButtonsProps {
isInAllView?: boolean; isInAllView?: boolean;
isInCatalogView?: boolean; isInCatalogView?: boolean;
isInSubscriptionsView?: boolean; isInSubscriptionsView?: boolean;
isInModView?: boolean;
isTopbar?: boolean; isTopbar?: boolean;
} }
const CatalogButton = ({ address, isInAllView, isInSubscriptionsView }: BoardButtonsProps) => { const CatalogButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
@@ -36,6 +37,9 @@ const CatalogButton = ({ address, isInAllView, isInSubscriptionsView }: BoardBut
} else if (isInSubscriptionsView) { } else if (isInSubscriptionsView) {
if (params?.timeFilterName) return `/p/subscriptions/catalog/${params.timeFilterName}`; if (params?.timeFilterName) return `/p/subscriptions/catalog/${params.timeFilterName}`;
return `/p/subscriptions/catalog`; return `/p/subscriptions/catalog`;
} else if (isInModView) {
if (params?.timeFilterName) return `/p/mod/catalog/${params.timeFilterName}`;
return `/p/mod/catalog`;
} }
return `/p/${address}/catalog`; return `/p/${address}/catalog`;
}; };
@@ -58,7 +62,7 @@ const SubscribeButton = ({ address }: BoardButtonsProps) => {
); );
}; };
const ReturnButton = ({ address, isInAllView, isInSubscriptionsView }: BoardButtonsProps) => { const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
@@ -69,6 +73,9 @@ const ReturnButton = ({ address, isInAllView, isInSubscriptionsView }: BoardButt
} else if (isInSubscriptionsView) { } else if (isInSubscriptionsView) {
if (params?.timeFilterName) return `/p/subscriptions/${params.timeFilterName}`; if (params?.timeFilterName) return `/p/subscriptions/${params.timeFilterName}`;
return `/p/subscriptions`; return `/p/subscriptions`;
} else if (isInModView) {
if (params?.timeFilterName) return `/p/mod/${params.timeFilterName}`;
return `/p/mod`;
} }
return `/p/${address}`; return `/p/${address}`;
}; };
@@ -190,7 +197,7 @@ const ShowOPCommentOption = () => {
); );
}; };
export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView, isTopbar = false }: BoardButtonsProps) => { export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView, isInModView, isTopbar = false }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { timeFilterName, timeFilterNames } = useTimeFilter(); const { timeFilterName, timeFilterNames } = useTimeFilter();
@@ -205,6 +212,10 @@ export const TimeFilter = ({ isInAllView, isInCatalogView, isInSubscriptionsView
? isInCatalogView ? isInCatalogView
? `/p/subscriptions/catalog/${timeFilterName}` ? `/p/subscriptions/catalog/${timeFilterName}`
: `/p/subscriptions/${timeFilterName}` : `/p/subscriptions/${timeFilterName}`
: isInModView
? isInCatalogView
? `/p/mod/catalog/${timeFilterName}`
: `/p/mod/${timeFilterName}`
: null; : null;
link && navigate(link); link && navigate(link);
}; };
@@ -242,6 +253,7 @@ export const MobileBoardButtons = () => {
const isInPendingPostPage = isPendingPostView(location.pathname, params); const isInPendingPostPage = isPendingPostView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress;
@@ -252,8 +264,8 @@ export const MobileBoardButtons = () => {
<div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}> <div className={`${styles.mobileBoardButtons} ${!isInCatalogView ? styles.addMargin : ''}`}>
{isInPostView || isInPendingPostPage ? ( {isInPostView || isInPendingPostPage ? (
<> <>
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} /> <ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} /> <CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
<SubscribeButton address={subplebbitAddress} /> <SubscribeButton address={subplebbitAddress} />
<div className={styles.secondRow}> <div className={styles.secondRow}>
<UpdateButton /> <UpdateButton />
@@ -263,11 +275,11 @@ export const MobileBoardButtons = () => {
) : ( ) : (
<> <>
{isInCatalogView ? ( {isInCatalogView ? (
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} /> <ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
) : ( ) : (
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} /> <CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
)} )}
{!(isInAllView || isInSubscriptionsView) && <SubscribeButton address={subplebbitAddress} />} {!(isInAllView || isInSubscriptionsView || isInModView) && <SubscribeButton address={subplebbitAddress} />}
<RefreshButton /> <RefreshButton />
{isInCatalogView && searchText ? ( {isInCatalogView && searchText ? (
<span className={styles.filteredThreadsCount}> <span className={styles.filteredThreadsCount}>
@@ -338,6 +350,7 @@ export const DesktopBoardButtons = () => {
const isInPendingPostPage = isPendingPostView(location.pathname, params); const isInPendingPostPage = isPendingPostView(location.pathname, params);
const isInPostView = isPostPageView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const { filteredCount, searchText } = useCatalogFiltersStore(); const { filteredCount, searchText } = useCatalogFiltersStore();
@@ -348,9 +361,9 @@ export const DesktopBoardButtons = () => {
{isInPostView || isInPendingPostPage ? ( {isInPostView || isInPendingPostPage ? (
<> <>
[ [
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} /> <ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
] [ ] [
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />] [ <CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />] [
<UpdateButton /> <UpdateButton />
] [ ] [
<AutoButton />] <AutoButton />]
@@ -363,12 +376,13 @@ export const DesktopBoardButtons = () => {
{isInCatalogView ? ( {isInCatalogView ? (
<> <>
[ [
<ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />]{' '} <ReturnButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]{' '}
</> </>
) : ( ) : (
<> <>
<SearchOPsBar />
[ [
<CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} />]{' '} <CatalogButton address={subplebbitAddress} isInAllView={isInAllView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />]{' '}
</> </>
)} )}
[<RefreshButton />] [<RefreshButton />]
@@ -394,10 +408,10 @@ export const DesktopBoardButtons = () => {
<ShowOPCommentOption /> <ShowOPCommentOption />
</> </>
)} )}
{(isInAllView || isInSubscriptionsView) && ( {(isInAllView || isInSubscriptionsView || isInModView) && (
<TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} /> <TimeFilter isInAllView={isInAllView} isInCatalogView={isInCatalogView} isInSubscriptionsView={isInSubscriptionsView} isInModView={isInModView} />
)} )}
{!(isInAllView || isInSubscriptionsView) && ( {!(isInAllView || isInSubscriptionsView || isInModView) && (
<> <>
[ [
<SubscribeButton address={subplebbitAddress} />] <SubscribeButton address={subplebbitAddress} />]
@@ -415,3 +429,42 @@ export const DesktopBoardButtons = () => {
</> </>
); );
}; };
const SearchOPsBar = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const params = useParams();
const location = useLocation();
const isInAllView = isAllView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname);
const handleSearch = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
const searchQuery = (event.target as HTMLInputElement).value.trim();
if (searchQuery) {
let catalogUrl = '';
if (isInAllView) {
catalogUrl = params?.timeFilterName
? `/p/all/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
: `/p/all/catalog?q=${encodeURIComponent(searchQuery)}`;
} else if (isInSubscriptionsView) {
catalogUrl = params?.timeFilterName
? `/p/subscriptions/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
: `/p/subscriptions/catalog?q=${encodeURIComponent(searchQuery)}`;
} else if (isInModView) {
catalogUrl = params?.timeFilterName
? `/p/mod/catalog/${params.timeFilterName}?q=${encodeURIComponent(searchQuery)}`
: `/p/mod/catalog?q=${encodeURIComponent(searchQuery)}`;
} else {
catalogUrl = `/p/${params?.subplebbitAddress}/catalog?q=${encodeURIComponent(searchQuery)}`;
}
navigate(catalogUrl);
}
}
};
return <input type='text' placeholder={t('search_ops_placeholder', 'Search OPs...')} onKeyDown={handleSearch} className={styles.searchOPsInput} />;
};
+17 -2
View File
@@ -1,6 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useParams } from 'react-router-dom'; import { useLocation, useParams } from 'react-router-dom';
import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import { useAccountComment } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils'; import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
import styles from './board-header.module.css'; import styles from './board-header.module.css';
@@ -9,6 +11,7 @@ import useIsMobile from '../../hooks/use-is-mobile';
import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline'; import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline';
import { shouldShowSnow } from '../../lib/snow'; import { shouldShowSnow } from '../../lib/snow';
import Tooltip from '../tooltip'; import Tooltip from '../tooltip';
import _ from 'lodash';
const totalBanners = 63; const totalBanners = 63;
@@ -22,6 +25,7 @@ const ImageBanner = () => {
}; };
const BoardHeader = () => { const BoardHeader = () => {
const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
const params = useParams(); const params = useParams();
const isInAllView = isAllView(location.pathname); const isInAllView = isAllView(location.pathname);
@@ -35,7 +39,13 @@ const BoardHeader = () => {
const { address, shortAddress } = subplebbit || {}; const { address, shortAddress } = subplebbit || {};
const multisubMetadata = useMultisubMetadata(); const multisubMetadata = useMultisubMetadata();
const title = isInAllView ? multisubMetadata?.title || 'all' : isInSubscriptionsView ? 'Subscriptions' : isInModView ? 'Mod' : subplebbit?.title; const title = isInAllView
? multisubMetadata?.title || 'all'
: isInSubscriptionsView
? 'Subscriptions'
: isInModView
? _.startCase(t('boards_you_moderate'))
: subplebbit?.title;
const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : isInModView ? 'p/mod' : `p/${address}`; const subtitle = isInAllView ? 'p/all' : isInSubscriptionsView ? 'p/subscriptions' : isInModView ? 'p/mod' : `p/${address}`;
const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit); const { isOffline, isOnlineStatusLoading, offlineIconClass, offlineTitle } = useIsSubplebbitOffline(subplebbit);
@@ -48,7 +58,12 @@ const BoardHeader = () => {
</div> </div>
)} )}
<div className={styles.boardTitle}> <div className={styles.boardTitle}>
{title || (shortAddress ? (shortAddress.endsWith('.eth') || shortAddress.endsWith('.sol') ? shortAddress.slice(0, -4) : shortAddress) : subplebbitAddress)} {title ||
(shortAddress
? shortAddress.endsWith('.eth') || shortAddress.endsWith('.sol')
? shortAddress.slice(0, -4)
: shortAddress
: Plebbit.getShortAddress(subplebbitAddress))}
{(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && !isInModView && ( {(isOffline || isOnlineStatusLoading) && !isInAllView && !isInSubscriptionsView && !isInModView && (
<span className={styles.offlineIconWrapper}> <span className={styles.offlineIconWrapper}>
<Tooltip content={offlineTitle}> <Tooltip content={offlineTitle}>
@@ -1,11 +1,11 @@
.button { .button {
text-transform: capitalize; text-transform: capitalize;
color: var(--button-desktop-text-color); color: var(--button-desktop-text-color, revert);
text-decoration: var(--button-text-decoration); text-decoration: var(--button-text-decoration, revert);
} }
.button:hover { .button:hover {
color: var(--button-desktop-text-color-hover); color: var(--button-desktop-text-color-hover, revert);
cursor: pointer; cursor: pointer;
} }
@@ -22,23 +22,23 @@
margin: 0px 5px; margin: 0px 5px;
padding: 1px; padding: 1px;
outline: none; outline: none;
border: 1px solid #AAA; border: var(--post-form-field-input-border, revert);
font-size: 11px; font-size: 11px;
} }
.searchContainer input:focus { .searchContainer input:focus {
border: var(--post-form-field-input-focus-border); border: var(--post-form-field-input-focus-border, revert);
} }
.closeSearch { .closeSearch {
cursor: pointer; cursor: pointer;
border: none; border: none;
white-space: nowrap; white-space: nowrap;
color: var(--button-desktop-text-color); color: var(--button-desktop-text-color, revert);
} }
.closeSearch:hover { .closeSearch:hover {
color: var(--button-desktop-text-color-hover); color: var(--button-desktop-text-color-hover, revert);
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -1,5 +1,6 @@
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import styles from './catalog-search.module.css'; import styles from './catalog-search.module.css';
import useIsMobile from '../../hooks/use-is-mobile'; import useIsMobile from '../../hooks/use-is-mobile';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
@@ -7,21 +8,52 @@ import _ from 'lodash';
const CatalogSearch = () => { const CatalogSearch = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const [openSearch, setOpenSearch] = useState(false); const [openSearch, setOpenSearch] = useState(false);
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const { setSearchFilter, clearSearchFilter } = useCatalogFiltersStore(); const { setSearchFilter, clearSearchFilter } = useCatalogFiltersStore();
// Create a debounced version of setSearchFilter // Extract query parameter from URL
useEffect(() => {
const urlParams = new URLSearchParams(location.search);
const queryParam = urlParams.get('q');
if (queryParam) {
setInputValue(queryParam);
setSearchFilter(queryParam);
setOpenSearch(true);
}
}, [location.search, setSearchFilter]);
// Update URL when search changes
const updateURL = useCallback(
(searchText: string) => {
const urlParams = new URLSearchParams(location.search);
if (searchText.trim()) {
urlParams.set('q', searchText);
} else {
urlParams.delete('q');
}
const newSearch = urlParams.toString();
const newPath = location.pathname + (newSearch ? `?${newSearch}` : '');
navigate(newPath, { replace: true });
},
[location.pathname, location.search, navigate],
);
// Create a debounced version of setSearchFilter and URL update
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedSetSearchFilter = useCallback( const debouncedSetSearchFilter = useCallback(
_.debounce((text: string) => { _.debounce((text: string) => {
if (text.trim()) { if (text.trim()) {
setSearchFilter(text); setSearchFilter(text);
updateURL(text);
} else { } else {
clearSearchFilter(); clearSearchFilter();
updateURL('');
} }
}, 300), }, 300),
[setSearchFilter, clearSearchFilter], [setSearchFilter, clearSearchFilter, updateURL],
); );
const handleToggleSearch = useCallback(() => { const handleToggleSearch = useCallback(() => {
@@ -30,14 +62,16 @@ const CatalogSearch = () => {
if (openSearch) { if (openSearch) {
setInputValue(''); setInputValue('');
clearSearchFilter(); clearSearchFilter();
updateURL('');
} }
}, [openSearch, clearSearchFilter]); }, [openSearch, clearSearchFilter, updateURL]);
const handleCloseSearch = useCallback(() => { const handleCloseSearch = useCallback(() => {
setOpenSearch(false); setOpenSearch(false);
setInputValue(''); setInputValue('');
clearSearchFilter(); clearSearchFilter();
}, [clearSearchFilter]); updateURL('');
}, [clearSearchFilter, updateURL]);
useEffect(() => { useEffect(() => {
if (openSearch) { if (openSearch) {
@@ -96,8 +96,8 @@ const Challenge = ({ challenge, closeModal }: ChallengeProps) => {
className={styles.container} className={styles.container}
ref={nodeRef} ref={nodeRef}
style={{ style={{
x: isMobile ? 0 : x, x: isMobile ? window.innerWidth / 2 - 150 : x,
y: isMobile ? 0 : y, y: isMobile ? window.innerHeight / 2 - 200 : y,
touchAction: 'none', touchAction: 'none',
}} }}
> >
+55 -7
View File
@@ -11,8 +11,9 @@ import { isCatalogView } from '../../lib/utils/view-utils';
import useIsMobile from '../../hooks/use-is-mobile'; import useIsMobile from '../../hooks/use-is-mobile';
import CommentMedia from '../comment-media'; import CommentMedia from '../comment-media';
import styles from './markdown.module.css'; import styles from './markdown.module.css';
import { useLocation, useParams } from 'react-router-dom'; import { Link, useLocation, useParams } from 'react-router-dom';
import { canEmbed } from '../embed'; import { canEmbed } from '../embed';
import { isPlebchanLink, transformPlebchanLinkToInternal, preprocessPlebchanPatterns } from '../../lib/utils/url-utils';
interface ContentLinkEmbedProps { interface ContentLinkEmbedProps {
children: any; children: any;
@@ -153,6 +154,52 @@ interface MarkdownProps {
title?: string; title?: string;
} }
const renderAnchorLink = (children: React.ReactNode, href: string) => {
if (!href) {
return <span>{children}</span>;
}
// Check if this is a valid plebchan link that should be handled internally
if (isPlebchanLink(href)) {
const internalPath = transformPlebchanLinkToInternal(href);
if (internalPath) {
// Check if the link text should be replaced with the internal path
let shouldReplaceText = false;
if (typeof children === 'string') {
shouldReplaceText = children === href || children.trim() === href.trim();
} else if (Array.isArray(children) && children.length === 1 && typeof children[0] === 'string') {
shouldReplaceText = children[0] === href || children[0].trim() === href.trim();
}
// For display purposes, remove leading slash from paths like "/p/something"
let displayText: React.ReactNode = children;
if (shouldReplaceText && internalPath.startsWith('/p/')) {
displayText = internalPath.substring(1); // Remove leading slash
} else if (shouldReplaceText) {
displayText = internalPath;
}
return <Link to={internalPath}>{displayText}</Link>;
} else {
console.warn('Failed to transform plebchan link to internal path:', href);
return <Link to={href}>{children}</Link>;
}
}
// Handle hash routes and internal patterns (including routes that start with /#/)
if (href.startsWith('#/') || href.startsWith('/#/') || href.startsWith('/p/') || href.match(/^\/p\/[^/]+(\/c\/[^/]+)?$/)) {
return <Link to={href}>{children}</Link>;
}
// External links
return (
<a href={href} target='_blank' rel='noopener noreferrer'>
{children}
</a>
);
};
const Markdown = ({ content, title }: MarkdownProps) => { const Markdown = ({ content, title }: MarkdownProps) => {
const remarkPlugins: any[] = [[supersub]]; const remarkPlugins: any[] = [[supersub]];
@@ -179,6 +226,9 @@ const Markdown = ({ content, title }: MarkdownProps) => {
const isInCatalogView = isCatalogView(useLocation().pathname, useParams()); const isInCatalogView = isCatalogView(useLocation().pathname, useParams());
// Preprocess content to convert plain text plebchan patterns to markdown links
const processedContent = preprocessPlebchanPatterns(content || '');
return ( return (
<span className={styles.markdown}> <span className={styles.markdown}>
{isInCatalogView && title && ( {isInCatalogView && title && (
@@ -188,7 +238,7 @@ const Markdown = ({ content, title }: MarkdownProps) => {
</span> </span>
)} )}
<ReactMarkdown <ReactMarkdown
children={content} children={processedContent}
remarkPlugins={remarkPlugins} remarkPlugins={remarkPlugins}
rehypePlugins={[[rehypeRaw as any], [rehypeSanitize, customSchema]]} rehypePlugins={[[rehypeRaw as any], [rehypeSanitize, customSchema]]}
components={ components={
@@ -220,12 +270,10 @@ const Markdown = ({ content, title }: MarkdownProps) => {
console.debug('Invalid URL:', href); console.debug('Invalid URL:', href);
} }
return ( return renderAnchorLink(children, href);
<a href={href} target='_blank' rel='noopener noreferrer'>
{children}
</a>
);
} }
return renderAnchorLink(children, href || '');
}, },
} as ExtendedComponents } as ExtendedComponents
} }
+23 -7
View File
@@ -1,13 +1,14 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { Comment, setAccount, useAccount, useAccountComment, useEditedComment } from '@plebbit/plebbit-react-hooks'; import { Comment, setAccount, useAccount, useAccountComment, useAccountSubplebbits, useEditedComment } from '@plebbit/plebbit-react-hooks';
import Plebbit from '@plebbit/plebbit-js';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils'; import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils';
import { formatMarkdown } from '../../lib/utils/post-utils'; import { formatMarkdown } from '../../lib/utils/post-utils';
import { isValidURL } from '../../lib/utils/url-utils'; import { isValidURL } from '../../lib/utils/url-utils';
import { isAllView, isDescriptionView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isDescriptionView, isModView, isPostPageView, isRulesView, isSubscriptionsView } from '../../lib/utils/view-utils';
import useAnonMode from '../../hooks/use-anon-mode'; import useAnonMode from '../../hooks/use-anon-mode';
import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits'; import { useDefaultSubplebbitAddresses } from '../../hooks/use-default-subplebbits';
import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame';
@@ -53,10 +54,14 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
const location = useLocation(); const location = useLocation();
const isInAllView = isAllView(location.pathname); const isInAllView = isAllView(location.pathname);
const isInModView = isModView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const subscriptions = account?.subscriptions || []; const subscriptions = account?.subscriptions || [];
const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses(); const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses();
const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
const { anonMode, getNewSigner, getExistingSigner } = useAnonMode(postCid); const { anonMode, getNewSigner, getExistingSigner } = useAnonMode(postCid);
const comment = useSubplebbitsPagesStore((state) => state.comments[postCid]); const comment = useSubplebbitsPagesStore((state) => state.comments[postCid]);
const address = comment?.author?.address; const address = comment?.author?.address;
@@ -129,7 +134,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
return; return;
} }
if ((isInAllView || isInSubscriptionsView) && !publishPostOptions.subplebbitAddress) { if ((isInAllView || isInSubscriptionsView || isInModView) && !publishPostOptions.subplebbitAddress) {
alert(t('no_board_selected_warning')); alert(t('no_board_selected_warning'));
return; return;
} }
@@ -394,7 +399,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
] ]
</td> </td>
</tr> </tr>
{(isInAllView || isInSubscriptionsView) && ( {(isInAllView || isInSubscriptionsView || isInModView) && (
<tr> <tr>
<td>{t('board')}</td> <td>{t('board')}</td>
<td> <td>
@@ -403,7 +408,13 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
{isInAllView && {isInAllView &&
defaultSubplebbitAddresses.map((address: string) => ( defaultSubplebbitAddresses.map((address: string) => (
<option key={address} value={address}> <option key={address} value={address}>
{address} {address && Plebbit.getShortAddress(address)}
</option>
))}
{isInModView &&
accountSubplebbitAddresses.map((address: string) => (
<option key={address} value={address}>
{address && Plebbit.getShortAddress(address)}
</option> </option>
))} ))}
{isInSubscriptionsView && {isInSubscriptionsView &&
@@ -429,6 +440,7 @@ const PostForm = () => {
const isInPostView = isPostPageView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params);
const isInRulesView = isRulesView(location.pathname, params); const isInRulesView = isRulesView(location.pathname, params);
const isInAllView = isAllView(location.pathname); const isInAllView = isAllView(location.pathname);
const isInModView = isModView(location.pathname);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
const commentCid = params?.commentCid; const commentCid = params?.commentCid;
@@ -453,7 +465,9 @@ const PostForm = () => {
return ( return (
<> <>
<div className={styles.postFormDesktop}> <div className={styles.postFormDesktop}>
{!(isInAllView || isInSubscriptionsView) && showForm && (isOffline || isOnlineStatusLoading) && <div className={styles.offlineBoard}>{offlineTitle}</div>} {!(isInAllView || isInSubscriptionsView || isInModView) && showForm && (isOffline || isOnlineStatusLoading) && (
<div className={styles.offlineBoard}>{offlineTitle}</div>
)}
{isThreadClosed ? ( {isThreadClosed ? (
<div className={styles.closed}> <div className={styles.closed}>
{t('thread_closed')} {t('thread_closed')}
@@ -473,7 +487,9 @@ const PostForm = () => {
)} )}
</div> </div>
<div className={styles.postFormMobile}> <div className={styles.postFormMobile}>
{!(isInAllView || isInSubscriptionsView) && showForm && (isOffline || isOnlineStatusLoading) && <div className={styles.offlineBoard}>{offlineTitle}</div>} {!(isInAllView || isInSubscriptionsView || isInModView) && showForm && (isOffline || isOnlineStatusLoading) && (
<div className={styles.offlineBoard}>{offlineTitle}</div>
)}
{isThreadClosed ? ( {isThreadClosed ? (
<div className={styles.closed}> <div className={styles.closed}>
{t('thread_closed')} {t('thread_closed')}
+17 -11
View File
@@ -1,5 +1,4 @@
import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js'; import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js';
import extName from 'ext-name';
import { canEmbed } from '../../components/embed'; import { canEmbed } from '../../components/embed';
import memoize from 'memoizee'; import memoize from 'memoizee';
import { isValidURL } from './url-utils'; import { isValidURL } from './url-utils';
@@ -72,6 +71,11 @@ const getPatternThumbnailUrl = (url: URL): string | undefined => {
} }
}; };
// Known media file extensions - only these will be classified as media files
const KNOWN_IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico', 'tiff'];
const KNOWN_VIDEO_EXTENSIONS = ['mp4', 'webm', 'mov', 'avi', 'mkv', 'flv', 'wmv', 'm4v'];
const KNOWN_AUDIO_EXTENSIONS = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a', 'wma'];
export const getLinkMediaInfo = memoize( export const getLinkMediaInfo = memoize(
(link: string): CommentMediaInfo | undefined => { (link: string): CommentMediaInfo | undefined => {
if (!isValidURL(link)) { if (!isValidURL(link)) {
@@ -80,7 +84,6 @@ export const getLinkMediaInfo = memoize(
const url = new URL(link); const url = new URL(link);
let patternThumbnailUrl: string | undefined; let patternThumbnailUrl: string | undefined;
let type: string = 'webpage'; let type: string = 'webpage';
let mime: string | undefined;
if (url.pathname === '/_next/image' && url.search.startsWith('?url=')) { if (url.pathname === '/_next/image' && url.search.startsWith('?url=')) {
return { url: link, type: 'image' }; return { url: link, type: 'image' };
@@ -93,16 +96,19 @@ export const getLinkMediaInfo = memoize(
} }
try { try {
mime = extName(url.pathname.toLowerCase().replace('/', ''))[0]?.mime; // Extract file extension
if (mime) { const pathParts = url.pathname.toLowerCase().split('.');
if (mime.startsWith('image')) { const extension = pathParts.length > 1 ? pathParts[pathParts.length - 1] : '';
type = mime === 'image/gif' ? 'gif' : 'image';
} else if (mime.startsWith('video')) { // Only classify as media if we explicitly know the extension
type = 'video'; if (KNOWN_IMAGE_EXTENSIONS.includes(extension)) {
} else if (mime.startsWith('audio')) { type = extension === 'gif' ? 'gif' : 'image';
type = 'audio'; } else if (KNOWN_VIDEO_EXTENSIONS.includes(extension)) {
} type = 'video';
} else if (KNOWN_AUDIO_EXTENSIONS.includes(extension)) {
type = 'audio';
} }
// Unknown extensions remain as 'webpage'
if (!url.pathname.includes('.')) { if (!url.pathname.includes('.')) {
type = 'webpage'; type = 'webpage';
+117
View File
@@ -21,3 +21,120 @@ export const copyShareLinkToClipboard = async (subplebbitAddress: string, cid: s
const shareLink = `https://pleb.bz/p/${subplebbitAddress}/c/${cid}?redirect=plebchan.app`; const shareLink = `https://pleb.bz/p/${subplebbitAddress}/c/${cid}?redirect=plebchan.app`;
await copyToClipboard(shareLink); await copyToClipboard(shareLink);
}; };
const PLEBCHAN_HOSTNAMES = ['pleb.bz', 'plebchan.app', 'plebchan.eth.limo', 'plebchan.eth.link', 'plebchan.eth.sucks', 'plebchan.netlify.app'];
// Check if a URL is a valid plebchan link that should be handled internally
export const isPlebchanLink = (url: string): boolean => {
try {
const parsedUrl = new URL(url);
const hostname = parsedUrl.hostname.replace(/^www\./, '');
if (!PLEBCHAN_HOSTNAMES.includes(hostname)) {
return false;
}
// Check both pathname and hash for the route pattern
let routePath = parsedUrl.pathname;
// If there's a hash that starts with #/, use that as the route path
if (parsedUrl.hash && parsedUrl.hash.startsWith('#/')) {
routePath = parsedUrl.hash.substring(1); // Remove the # to get the path
}
// For pleb.bz, only support the exact sharelink format
if (hostname === 'pleb.bz') {
// Must match exactly: /p/{subplebbitAddress}/c/{cid}
// Allow redirect parameter since these are still valid internal links
return /^\/p\/[^/]+\/c\/[^/]+$/.test(routePath);
}
// For other plebchan hostnames, support:
// - /p/{subplebbitAddress}
// - /p/{subplebbitAddress}/c/{commentCid}
return /^\/p\/[^/]+(\/c\/[^/]+)?$/.test(routePath);
} catch {
return false;
}
};
// Transform a valid plebchan URL to an internal route
export const transformPlebchanLinkToInternal = (url: string): string | null => {
if (!isPlebchanLink(url)) {
return null;
}
try {
const parsedUrl = new URL(url);
// Check if this is a hash-based route
if (parsedUrl.hash && parsedUrl.hash.startsWith('#/')) {
// Extract the route from the hash, preserving any query params within the hash
const hashPath = parsedUrl.hash.substring(1); // Remove the #
return hashPath;
}
// For regular pathname-based routes, remove redirect parameter from query string
const searchParams = new URLSearchParams(parsedUrl.search);
searchParams.delete('redirect'); // Remove redirect parameter for cleaner internal links
const cleanSearch = searchParams.toString();
const searchString = cleanSearch ? `?${cleanSearch}` : '';
return parsedUrl.pathname + searchString + parsedUrl.hash;
} catch {
return null;
}
};
// Check if a string is a valid IPNS public key (52 chars starting with 12D3KooW)
const isValidIPNSKey = (str: string): boolean => {
return str.length === 52 && str.startsWith('12D3KooW');
};
// Check if a string is a valid domain (contains a dot)
const isValidDomain = (str: string): boolean => {
return str.includes('.') && str.split('.').length >= 2 && str.split('.').every((part) => part.length > 0);
};
// Check if a plain text pattern is a valid plebchan subplebbit reference
export const isValidSubplebbitPattern = (pattern: string): boolean => {
// Must start with "p/"
if (!pattern.startsWith('p/')) {
return false;
}
const pathPart = pattern.substring(2); // Remove "p/"
// Check if it's a post pattern: subplebbitAddress/c/cid
const postMatch = pathPart.match(/^([^/]+)\/c\/([^/]+)$/);
if (postMatch) {
const [, subplebbitAddress, cid] = postMatch;
// CID should be at least 10 characters (minimum reasonable CID length)
return (isValidDomain(subplebbitAddress) || isValidIPNSKey(subplebbitAddress)) && cid.length >= 10;
}
// Check if it's just a subplebbit pattern: subplebbitAddress
return isValidDomain(pathPart) || isValidIPNSKey(pathPart);
};
// Preprocess content to convert plain text plebchan patterns to markdown links
export const preprocessPlebchanPatterns = (content: string): string => {
// Pattern to match "p/something" or "p/something/c/something"
// Negative lookbehind prevents matching patterns that are already part of URLs
const pattern = /(?<!https?:\/\/[^\s]*)\bp\/([a-zA-Z0-9\-.]+(?:\/c\/[a-zA-Z0-9]{10,100})?)[.,:;!?]*/g;
return content.replace(pattern, (match, capturedPath) => {
// Remove any trailing punctuation from the captured path
const cleanPath = capturedPath.replace(/[.,:;!?]+$/, '');
const fullPattern = `p/${cleanPath}`;
if (isValidSubplebbitPattern(fullPattern)) {
// Preserve trailing punctuation outside the link
const trailingPunctuation = match.slice(fullPattern.length);
return `[${fullPattern}](/${fullPattern})${trailingPunctuation}`;
}
return match;
});
};
-2
View File
@@ -3,8 +3,6 @@ declare module '*.module.css' {
export default classes; export default classes;
} }
declare module 'ext-name';
declare module 'lodash'; declare module 'lodash';
declare module 'react-draggable'; declare module 'react-draggable';
+5 -3
View File
@@ -148,15 +148,17 @@ const Catalog = () => {
} }
return true; return true;
}) })
.map((subplebbit) => subplebbit.address); .map((subplebbit) => subplebbit.address)
.filter(Boolean); // Filter out any undefined/null values
if (isInAllView) { if (isInAllView) {
return filteredDefaultSubplebbits; return filteredDefaultSubplebbits;
} }
if (isInSubscriptionsView) { if (isInSubscriptionsView) {
return subscriptions || []; return (subscriptions || []).filter(Boolean); // Filter out any undefined/null values
} }
return [subplebbitAddress]; // Only include subplebbitAddress if it's defined
return subplebbitAddress ? [subplebbitAddress] : [];
}, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbits, subscriptions, hideAdultBoards, hideGoreBoards]); }, [isInAllView, isInSubscriptionsView, subplebbitAddress, defaultSubplebbits, subscriptions, hideAdultBoards, hideGoreBoards]);
const { imageSize } = useCatalogStyleStore(); const { imageSize } = useCatalogStyleStore();