feat(catalog filter): add filtering with complex patterns, including regex, help modal

This commit is contained in:
Tom (plebeius.eth)
2025-03-06 23:48:17 +01:00
parent ad60404792
commit d7a9c1640b
45 changed files with 500 additions and 85 deletions

Before

Width:  |  Height:  |  Size: 150 B

After

Width:  |  Height:  |  Size: 150 B

Before

Width:  |  Height:  |  Size: 138 B

After

Width:  |  Height:  |  Size: 138 B

Before

Width:  |  Height:  |  Size: 147 B

After

Width:  |  Height:  |  Size: 147 B

Before

Width:  |  Height:  |  Size: 157 B

After

Width:  |  Height:  |  Size: 157 B

+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "تمت إزالة هذه الرد من قبل مشرفي المنتدى",
"not_mod_of_any_board": "أنت لست مشرفًا في أي لوحة.",
"filters_and_highlights": "الفلاتر والإبرازات",
"search_results_for": "نتائج البحث عن"
"search_results_for": "نتائج البحث عن",
"help": "مساعدة",
"filter_and_highlights_help": "مساعدة الفلاتر والإبرازات",
"filter_and_highlights": "التصفية والتسليط الضوء"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "এই প্রতিক্রিয়া বোর্ড পরিচালকদের দ্বারা সরানো হয়েছে",
"not_mod_of_any_board": "আপনি কোনও বোর্ডের মড নন।",
"filters_and_highlights": "ফিল্টার এবং হাইলাইট",
"search_results_for": "অনুসন্ধান ফলাফল"
"search_results_for": "অনুসন্ধান ফলাফল",
"help": "সাহায্য",
"filter_and_highlights_help": "ফিল্টার এবং হাইলাইট সাহায্য",
"filter_and_highlights": "ফিল্টার এবং হাইলাইট"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Tato odpověď byla odstraněna moderátory fóra",
"not_mod_of_any_board": "Nejste moderátor žádné desky.",
"filters_and_highlights": "Filtry a zvýraznění",
"search_results_for": "Výsledky hledání pro"
"search_results_for": "Výsledky hledání pro",
"help": "Pomoc",
"filter_and_highlights_help": "Pomoc s filtry a zvýrazněním",
"filter_and_highlights": "Filtry a zvýraznění"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Dette svar blev fjernet af forummoderatorer",
"not_mod_of_any_board": "Du er ikke moderator for nogen tavler.",
"filters_and_highlights": "Filtre og fremhævninger",
"search_results_for": "Søgeresultater for"
"search_results_for": "Søgeresultater for",
"help": "Hjælp",
"filter_and_highlights_help": "Filtre og fremhævelser Hjælp",
"filter_and_highlights": "Filtre og fremhævninger"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Diese Antwort wurde von den Forenmoderatoren entfernt",
"not_mod_of_any_board": "Du bist kein Moderator eines Boards.",
"filters_and_highlights": "Filter & Highlights",
"search_results_for": "Suchergebnisse für"
"search_results_for": "Suchergebnisse für",
"help": "Hilfe",
"filter_and_highlights_help": "Filter- und Markierungs-Hilfe",
"filter_and_highlights": "Filter und Hervorhebungen"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Η απάντηση αυτή αφαιρέθηκε από τους διαχειριστές του φόρουμ",
"not_mod_of_any_board": "Δεν είστε διαχειριστής σε καμία κοινότητα.",
"filters_and_highlights": "Φίλτρα και επισημάνσεις",
"search_results_for": "Αποτελέσματα αναζήτησης για"
"search_results_for": "Αποτελέσματα αναζήτησης για",
"help": "Βοήθεια",
"filter_and_highlights_help": "Βοήθεια φίλτρων και επισήμανσης",
"filter_and_highlights": "Φίλτρα και Επισημάνσεις"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "This reply was removed by board moderators",
"not_mod_of_any_board": "You are not a mod of any board.",
"filters_and_highlights": "Filters & Highlights",
"search_results_for": "Search results for"
"search_results_for": "Search results for",
"help": "Help",
"filter_and_highlights_help": "Filters & Highlights Help",
"filter_and_highlights": "Filters & Highlights"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Esta respuesta fue eliminada por los moderadores del foro",
"not_mod_of_any_board": "No eres moderador de ningún tablero.",
"filters_and_highlights": "Filtros y resaltar",
"search_results_for": "Resultados de búsqueda para"
"search_results_for": "Resultados de búsqueda para",
"help": "Ayuda",
"filter_and_highlights_help": "Ayuda de filtros y resaltes",
"filter_and_highlights": "Filtros y resaltados"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "این پاسخ توسط مدیران انجمن حذف شد",
"not_mod_of_any_board": "شما هیچ تابلو مدیر نیستید.",
"filters_and_highlights": "فیلترها و هایلایت‌ها",
"search_results_for": "نتایج جستجو برای"
"search_results_for": "نتایج جستجو برای",
"help": "کمک",
"filter_and_highlights_help": "کمک فیلترها و برجسته‌ها",
"filter_and_highlights": "فیلترها و برجسته‌ها"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Tämä vastaus poistettiin foorumin moderaattoreiden toimesta",
"not_mod_of_any_board": "Et ole moderaattori missään taulussa.",
"filters_and_highlights": "Suodattimet ja korostukset",
"search_results_for": "Hakutulokset hakusanalle"
"search_results_for": "Hakutulokset hakusanalle",
"help": "Apua",
"filter_and_highlights_help": "Suodattimet ja korostukset -apu",
"filter_and_highlights": "Suodattimet ja korostukset"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Ang sagot na ito ay tinanggal ng mga moderator ng board",
"not_mod_of_any_board": "Hindi ka mod ng anumang board.",
"filters_and_highlights": "Mga filter at highlight",
"search_results_for": "Mga resulta ng paghahanap para sa"
"search_results_for": "Mga resulta ng paghahanap para sa",
"help": "Tulong",
"filter_and_highlights_help": "Tulong sa mga filter at highlight",
"filter_and_highlights": "Mga filter at highlight"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Cette réponse a été supprimée par les modérateurs du forum",
"not_mod_of_any_board": "Vous n'êtes pas modérateur de n'aucun forum.",
"filters_and_highlights": "Filtres et mises en évidence",
"search_results_for": "Résultats de recherche pour"
"search_results_for": "Résultats de recherche pour",
"help": "Aide",
"filter_and_highlights_help": "Aide sur les filtres et surlignages",
"filter_and_highlights": "Filtres et surlignages"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "תשובה זו הוסרה על ידי מנהלי הפורום",
"not_mod_of_any_board": "אתה לא מוד בכל לוח.",
"filters_and_highlights": "סינונים והדגשות",
"search_results_for": "תוצאות חיפוש עבור"
"search_results_for": "תוצאות חיפוש עבור",
"help": "עזרה",
"filter_and_highlights_help": "עזרה במסננים והדגשות",
"filter_and_highlights": "מסננים והדגשות"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "यह उत्तर बोर्ड मॉडरेटर द्वारा हटा दिया गया था",
"not_mod_of_any_board": "आप किसी बोर्ड के मॉड नहीं हैं।",
"filters_and_highlights": "फ़िल्टर और हाइलाइट्स",
"search_results_for": "के लिए खोज परिणाम"
"search_results_for": "के लिए खोज परिणाम",
"help": "मदद",
"filter_and_highlights_help": "फ़िल्टर और हाइलाइट्स सहायता",
"filter_and_highlights": "फ़िल्टर और हाइलाइट्स"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Ez a válasz törlésre került a fórum moderátorai által",
"not_mod_of_any_board": "Nem vagy moderátor egyetlen táblán sem.",
"filters_and_highlights": "Szűrők és kiemelések",
"search_results_for": "Keresési eredmények"
"search_results_for": "Keresési eredmények",
"help": "Segítség",
"filter_and_highlights_help": "Szűrők és kiemelések súgó",
"filter_and_highlights": "Szűrők és kiemelések"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Balasan ini dihapus oleh moderator papan",
"not_mod_of_any_board": "Anda bukan mod di papan manapun.",
"filters_and_highlights": "Filter dan Sorotan",
"search_results_for": "Hasil pencarian untuk"
"search_results_for": "Hasil pencarian untuk",
"help": "Bantuan",
"filter_and_highlights_help": "Bantuan Filter dan Sorotan",
"filter_and_highlights": "Filter dan Sorotan"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Questa risposta è stata rimossa dai moderatori del forum",
"not_mod_of_any_board": "Non sei un mod di nessuna board.",
"filters_and_highlights": "Filtri e Evidenziazioni",
"search_results_for": "Risultati di ricerca per"
"search_results_for": "Risultati di ricerca per",
"help": "Aiuto",
"filter_and_highlights_help": "Aiuto su Filtri e Evidenziazioni",
"filter_and_highlights": "Filtri e Evidenziazioni"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "この返信は掲示板のモデレーターによって削除されました",
"not_mod_of_any_board": "あなたはどのボードのモデレーターでもありません。",
"filters_and_highlights": "フィルターとハイライト",
"search_results_for": "検索結果"
"search_results_for": "検索結果",
"help": "ヘルプ",
"filter_and_highlights_help": "フィルターとハイライトのヘルプ",
"filter_and_highlights": "フィルターとハイライト"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "이 답글은 게시판 관리자에 의해 삭제되었습니다",
"not_mod_of_any_board": "당신은 어떤 게시판의 관리자도 아닙니다.",
"filters_and_highlights": "필터 및 하이라이트",
"search_results_for": "검색 결과"
"search_results_for": "검색 결과",
"help": "도움말",
"filter_and_highlights_help": "필터 및 하이라이트 도움말",
"filter_and_highlights": "필터 및 하이라이트"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "या उत्तराला बोर्ड मॉडरेटरने काढले आहे",
"not_mod_of_any_board": "तुम्ही कोणत्याही बोर्डचे मॉड नाहीत.",
"filters_and_highlights": "फिल्टर आणि हायलाइट्स",
"search_results_for": "सर्च परिणाम"
"search_results_for": "सर्च परिणाम",
"help": "मदत",
"filter_and_highlights_help": "फिल्टर आणि हायलाइट्स सहाय्य",
"filter_and_highlights": "फिल्टर आणि हायलाइट्स"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Dit antwoord is verwijderd door de forum moderators",
"not_mod_of_any_board": "Je bent geen mod van een bord.",
"filters_and_highlights": "Filters en markeringen",
"search_results_for": "Zoekresultaten voor"
"search_results_for": "Zoekresultaten voor",
"help": "Help",
"filter_and_highlights_help": "Filters en Markeringen Help",
"filter_and_highlights": "Filters en markeringen"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Dette svaret ble fjernet av forummoderatorer",
"not_mod_of_any_board": "Du er ikke moderator på noen brett.",
"filters_and_highlights": "Filtre og fremhevinger",
"search_results_for": "Søkeresultater for"
"search_results_for": "Søkeresultater for",
"help": "Hjelp",
"filter_and_highlights_help": "Filtre og fremhevinger Hjelp",
"filter_and_highlights": "Filtre og uthevinger"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Ta odpowiedź została usunięta przez moderatorów forum",
"not_mod_of_any_board": "Nie jesteś moderatorem żadnej tablicy.",
"filters_and_highlights": "Filtry i wyróżnienia",
"search_results_for": "Wyniki wyszukiwania dla"
"search_results_for": "Wyniki wyszukiwania dla",
"help": "Pomoc",
"filter_and_highlights_help": "Pomoc dotycząca filtrów i wyróżnień",
"filter_and_highlights": "Filtry i wyróżnienia"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Esta resposta foi removida pelos moderadores do fórum",
"not_mod_of_any_board": "Você não é um moderador de nenhum quadro.",
"filters_and_highlights": "Filtros e Destaques",
"search_results_for": "Resultados de pesquisa para"
"search_results_for": "Resultados de pesquisa para",
"help": "Ajuda",
"filter_and_highlights_help": "Ajuda de Filtros e Realces",
"filter_and_highlights": "Filtros e Destaques"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Acest răspuns a fost eliminat de moderatorii forumului",
"not_mod_of_any_board": "Nu ești moderator pe niciun tablou.",
"filters_and_highlights": "Filtre și evidențiere",
"search_results_for": "Rezultatele căutării pentru"
"search_results_for": "Rezultatele căutării pentru",
"help": "Ajutor",
"filter_and_highlights_help": "Ajutor pentru filtre și evidențieri",
"filter_and_highlights": "Filtre și evidențieri"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Этот ответ был удален модераторами форума",
"not_mod_of_any_board": "Вы не модератор ни одной доски.",
"filters_and_highlights": "Фильтры и выделения",
"search_results_for": "Результаты поиска для"
"search_results_for": "Результаты поиска для",
"help": "Помощь",
"filter_and_highlights_help": "Помощь с фильтрами и выделениями",
"filter_and_highlights": "Фильтры и выделения"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Ky përgjigje u hoq nga moderatorët e forumit",
"not_mod_of_any_board": "Nuk jeni mod në asnjë bord.",
"filters_and_highlights": "Filtra dhe theksime",
"search_results_for": "Rezultatet e kërkimit për"
"search_results_for": "Rezultatet e kërkimit për",
"help": "Ndihmë",
"filter_and_highlights_help": "Ndihma për Filtro & Thekso",
"filter_and_highlights": "Filtra dhe thekson"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Detta svar togs bort av forumets moderatorer",
"not_mod_of_any_board": "Du är inte moderator på någon tavla.",
"filters_and_highlights": "Filter och markeringar",
"search_results_for": "Sökresultat för"
"search_results_for": "Sökresultat för",
"help": "Hjälp",
"filter_and_highlights_help": "Filter och markeringar Hjälp",
"filter_and_highlights": "Filter och markeringar"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "ఈ స్పందన బోర్డ్ మోడరేటర్లచే తొలగించబడింది",
"not_mod_of_any_board": "మీరు ఎటువంటి బోర్డుకు మోడ్ కంటే లేరు.",
"filters_and_highlights": "ఫిల్టర్లు మరియు హైలైట్‌లు",
"search_results_for": "శోధన ఫలితాలు"
"search_results_for": "శోధన ఫలితాలు",
"help": "సహాయం",
"filter_and_highlights_help": "ఫిల్టర్లు మరియు హైలైట్ సహాయం",
"filter_and_highlights": "ఫిల్టర్లు మరియు హైలైట్స్"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "คำตอบนี้ถูกลบโดยผู้ดูแลบอร์ด",
"not_mod_of_any_board": "คุณไม่ใช่ผู้ดูแลบอร์ดใดๆ",
"filters_and_highlights": "ตัวกรองและไฮไลท์",
"search_results_for": "ผลการค้นหาสำหรับ"
"search_results_for": "ผลการค้นหาสำหรับ",
"help": "ช่วยเหลือ",
"filter_and_highlights_help": "ตัวกรองและการเน้นความช่วยเหลือ",
"filter_and_highlights": "ตัวกรองและไฮไลท์"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Bu yanıt, forum moderatörleri tarafından kaldırıldı",
"not_mod_of_any_board": "Hiçbir tahtanın moderatörü değilsiniz.",
"filters_and_highlights": "Filtreler ve Vurgular",
"search_results_for": "Arama sonuçları için"
"search_results_for": "Arama sonuçları için",
"help": "Yardım",
"filter_and_highlights_help": "Filtreler ve Vurgular Yardımı",
"filter_and_highlights": "Filtreler ve Vurgular"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Цю відповідь було видалено модераторами форуму",
"not_mod_of_any_board": "Ви не модератор жодної дошки.",
"filters_and_highlights": "Фільтри та виділення",
"search_results_for": "Результати пошуку для"
"search_results_for": "Результати пошуку для",
"help": "Допомога",
"filter_and_highlights_help": "Допомога з фільтрами та підсвічуваннями",
"filter_and_highlights": "Фільтри та виділення"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "اس جواب کو بورڈ کے منتظمین نے حذف کر دیا ہے",
"not_mod_of_any_board": "آپ کسی بورڈ کے موڈ نہیں ہیں۔",
"filters_and_highlights": "فلٹرز اور ہائیلائٹس",
"search_results_for": "تلاش کے نتائج"
"search_results_for": "تلاش کے نتائج",
"help": "مدد",
"filter_and_highlights_help": "فلٹرز اور ہائی لائٹس کی مدد",
"filter_and_highlights": "فلٹرز اور ہائلाइटس"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "Phản hồi này đã bị xóa bởi các quản trị viên diễn đàn",
"not_mod_of_any_board": "Bạn không phải là quản trị viên của bất kỳ bảng nào.",
"filters_and_highlights": "Bộ lọc và Nổi bật",
"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",
"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"
}
+4 -1
View File
@@ -293,5 +293,8 @@
"this_reply_was_removed": "该回复已被版主删除",
"not_mod_of_any_board": "您不是任何板块的管理员。",
"filters_and_highlights": "筛选器和高亮",
"search_results_for": "搜索结果"
"search_results_for": "搜索结果",
"help": "帮助",
"filter_and_highlights_help": "过滤器和高亮帮助",
"filter_and_highlights": "筛选器与高亮"
}
@@ -33,7 +33,7 @@
.modal {
top: 60px;
left: 50%;
left: calc(50% + 5px);
transform: translateX(-50%);
width: 380px;
height: auto;
@@ -41,14 +41,23 @@
overflow-y: auto;
position: fixed;
z-index: 5;
padding: 2px;
padding: 2px 5px 5px 5px;
font-size: var(--settings-modal-font-size);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
background-color: var(--settings-modal-background-color);
border-top: var(--settings-modal-border-top);
border-right: var(--settings-modal-border-right);
border-bottom: var(--settings-modal-border-bottom);
border-left: var(--settings-modal-border-left);
}
.filtersProtipModal {
top: 50px;
width: 570px;
left: 50%;
padding: 2px 10px 5px 10px;
}
@media (max-width: 614px) {
.filtersProtipModal {
width: 90vw;
}
}
@media (max-width: 420px) {
@@ -82,6 +91,54 @@
top: 5px;
image-rendering: pixelated;
background-image: var(--settings-modal-close-button-background-image);
cursor: pointer;
}
.openHelpButton {
right: 23px;
width: 18px;
height: 18px;
display: block;
background-size: 100%;
position: absolute;
top: 5px;
image-rendering: pixelated;
background-image: var(--open-help-button-background-image);
cursor: pointer;
}
.filtersProtip {
text-transform: none;
}
.filtersProtip code {
padding: 1px 5px 1px 5px;
background-color: #EEE;
color: #000;
}
.filtersProtip li {
list-style: none;
margin: 5px 0px;
}
.filtersProtip ul {
margin-bottom: 10px;
}
.filtersProtip h4 {
font-size: 15px;
margin: 10px 0px 5px 0px;
padding-top: 10px;
}
.filtersProtip h4:first-child {
padding-top: 0px;
}
.filtersProtip h4::before {
content: "»";
margin-right: 3px;
}
.filters {
@@ -108,16 +165,12 @@
padding-bottom: 5px;
}
.saveButton {
margin-left: 5px;
}
.filtersTable {
width: 100%;
border-spacing: 1px;
margin-left: auto;
margin-right: auto;
padding: 10px 2px 2px 2px;
padding: 0px 2px 2px 2px;
}
.filtersTable th {
@@ -168,7 +221,8 @@
}
.saveButton {
float: right;
position: absolute;
right: 5px;
}
.filterHits {
@@ -1,11 +1,13 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import useFeedResetStore from '../../stores/use-feed-reset-store';
import styles from './catalog-filters.module.css';
const FiltersTable = ({ onSave }: { onSave: () => void }) => {
const { t } = useTranslation();
const { filterItems, saveAndApplyFilters, currentSubplebbitAddress } = useCatalogFiltersStore();
const resetFeed = useFeedResetStore((state) => state.reset);
const [localFilterItems, setLocalFilterItems] = useState(
filterItems.map((item) => ({
@@ -52,9 +54,17 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
const handleSave = useCallback(() => {
const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== '');
saveAndApplyFilters(nonEmptyFilters);
useCatalogFiltersStore.getState().resetCountsForCurrentSubplebbit();
if (resetFeed) {
resetFeed();
}
onSave();
}, [saveAndApplyFilters, localFilterItems, onSave]);
}, [saveAndApplyFilters, localFilterItems, onSave, resetFeed]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
@@ -142,7 +152,7 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
</span>
</td>
<td className={styles.filterHits}>
{currentSubplebbitAddress && (item.subplebbitCounts?.get(currentSubplebbitAddress) ?? 0) > 0 && `x${item.subplebbitCounts?.get(currentSubplebbitAddress)}`}
{currentSubplebbitAddress && item.subplebbitFilteredCids?.has(currentSubplebbitAddress) && `x${item.subplebbitCounts?.get(currentSubplebbitAddress) ?? 0}`}
</td>
</tr>
))}
@@ -163,18 +173,107 @@ const FiltersTable = ({ onSave }: { onSave: () => void }) => {
);
};
const FiltersProtip = () => {
return (
<div className={styles.filtersProtip}>
<h4>Patterns</h4>
<ul>
<li>
<strong>Matching whole words:</strong>
</li>
<li>
<code>feel</code> will match <em>"feel"</em> but not <em>"feeling"</em>. This search is case-insensitive.
</li>
</ul>
<ul>
<li>
<strong>AND operator:</strong>
</li>
<li>
<code>feel girlfriend</code> will match <em>"feel"</em> AND <em>"girlfriend"</em> in any order.
</li>
</ul>
<ul>
<li>
<strong>OR operator:</strong>
</li>
<li>
<code>feel|girlfriend</code> will match <em>"feel"</em> OR <em>"girlfriend"</em>.
</li>
</ul>
<ul>
<li>
<strong>Mixing both operators:</strong>
</li>
<li>
<code>girlfriend|boyfriend feel</code> matches <em>"feel"</em> AND <em>"girlfriend"</em>, or <em>"feel"</em> AND <em>"boyfriend"</em>.
</li>
</ul>
<ul>
<li>
<strong>Exact match search:</strong>
</li>
<li>
<code>"that feel when"</code> place double quotes around the pattern to search for an exact string.
</li>
</ul>
<ul>
<li>
<strong>Wildcards:</strong>
</li>
<li>
<code>feel*</code> matches expressions such as <em>"feel"</em>, <em>"feels"</em>, <em>"feeling"</em>, <em>"feeler"</em>, etc
</li>
<li>
<code>idolm*ster</code> this can match <em>"idolmaster"</em> or <em>"idolm@ster"</em>, etc
</li>
</ul>
<ul>
<strong>It is also possible to filter by regular expression:</strong>
<li>
<code>/^(?=.*detachable)(?=.*hats).*$/i</code> AND operator.
</li>
<li>
<code>/^(?!.*touhou).*$/i</code> NOT operator.
</li>
<li>
<code>{'/^&gt;/'}</code> threads starting with a quote (<em>{'">"'}</em> character as an html entity).
</li>
<li>
<code>/^$/</code> threads with no text.
</li>
</ul>
<h4>Controls</h4>
<ul>
<li>
<strong>On</strong> enables or disables the filter.
</li>
<li>
<strong>Hide</strong> hides matched threads.
</li>
<li>
<strong>Top</strong> moves the filter to the top of the feed.
</li>
</ul>
</div>
);
};
const FiltersModal = ({ closeModal }: { closeModal: () => void }) => {
const { t } = useTranslation();
const [showHelp, setShowHelp] = useState(false);
const openHelp = () => setShowHelp(true);
const closeHelp = () => setShowHelp(false);
return (
<>
<div className={styles.overlay} onClick={closeModal} />
<div className={styles.modal}>
<div className={styles.overlay} onClick={showHelp ? closeHelp : closeModal} />
<div className={`${styles.modal} ${showHelp ? styles.filtersProtipModal : ''}`}>
<div className={styles.header}>
<span className={styles.title}>{t('filters_and_highlights')}</span>
<span className={styles.closeButton} title='close' onClick={closeModal} />
<span className={styles.title}>{showHelp ? t('filter_and_highlights_help') : t('filter_and_highlights')}</span>
{!showHelp && <span className={styles.openHelpButton} title={t('help')} onClick={openHelp} />}
<span className={styles.closeButton} title={t('close')} onClick={closeModal} />
</div>
<FiltersTable onSave={closeModal} />
{showHelp ? <FiltersProtip /> : <FiltersTable onSave={closeModal} />}
</div>
</>
);
+110
View File
@@ -0,0 +1,110 @@
import { Comment } from '@plebbit/plebbit-react-hooks';
/**
* Checks if a text matches a pattern according to various pattern matching rules:
* - Whole word matching: 'feel' matches 'feel' but not 'feeling'
* - AND operator: 'feel girlfriend' matches both words in any order
* - OR operator: 'feel|girlfriend' matches either word
* - Mixed operators: 'girlfriend|boyfriend feel' matches complex logic
* - Exact match: "that feel when" matches the exact phrase
* - Wildcards: 'feel*' matches 'feel', 'feels', 'feeling', etc.
* - Regex: /pattern/flags for regular expressions
*
* @param text The text to check against the pattern
* @param pattern The pattern to match
* @returns True if the text matches the pattern, false otherwise
*/
export const matchesPattern = (text: string, pattern: string): boolean => {
if (!pattern || !text) return false;
const textLower = text.toLowerCase();
try {
// Check if it's a regex pattern (starts and ends with /)
if (pattern.startsWith('/') && pattern.length > 2 && /\/[gimsuy]*$/.test(pattern)) {
const lastSlashIndex = pattern.lastIndexOf('/');
const regexPattern = pattern.substring(1, lastSlashIndex);
const flags = pattern.substring(lastSlashIndex + 1);
const regex = new RegExp(regexPattern, flags);
return regex.test(textLower);
}
// Check if it's an exact match pattern (surrounded by quotes)
else if (pattern.startsWith('"') && pattern.endsWith('"') && pattern.length > 2) {
const exactPattern = pattern.substring(1, pattern.length - 1).toLowerCase();
return textLower.includes(exactPattern);
}
// Check for OR patterns (contains |)
else if (pattern.includes('|')) {
const orGroups = pattern.split(' ').filter(Boolean);
// Process each group (which might contain OR operators)
return orGroups.every((group) => {
const orTerms = group.split('|').map((term) => term.toLowerCase());
return orTerms.some((term) => {
// Handle wildcards in OR terms
if (term.includes('*')) {
const regexPattern = term.replace(/\*/g, '.*').toLowerCase();
const regex = new RegExp(`\\b${regexPattern}\\b`, 'i');
return regex.test(textLower);
} else {
// Match whole word only
const regex = new RegExp(`\\b${term}\\b`, 'i');
return regex.test(textLower);
}
});
});
}
// Check for AND patterns (space-separated words)
else if (pattern.includes(' ')) {
const andTerms = pattern
.split(' ')
.filter(Boolean)
.map((term) => term.toLowerCase());
return andTerms.every((term) => {
// Handle wildcards in AND terms
if (term.includes('*')) {
const regexPattern = term.replace(/\*/g, '.*').toLowerCase();
const regex = new RegExp(`\\b${regexPattern}\\b`, 'i');
return regex.test(textLower);
} else {
// Match whole word only
const regex = new RegExp(`\\b${term}\\b`, 'i');
return regex.test(textLower);
}
});
}
// Handle wildcard patterns
else if (pattern.includes('*')) {
const regexPattern = pattern.replace(/\*/g, '.*').toLowerCase();
const regex = new RegExp(`\\b${regexPattern}\\b`, 'i');
return regex.test(textLower);
}
// Simple whole word match
else {
const regex = new RegExp(`\\b${pattern.toLowerCase()}\\b`, 'i');
return regex.test(textLower);
}
} catch (error) {
// If regex parsing fails, fall back to simple includes
console.error('Pattern matching error:', error);
const simplifiedPattern = pattern.toLowerCase();
return textLower.includes(simplifiedPattern);
}
};
/**
* Checks if a comment matches a pattern in its title or content
*
* @param comment The comment to check
* @param pattern The pattern to match
* @returns True if the comment matches the pattern, false otherwise
*/
export const commentMatchesPattern = (comment: Comment, pattern: string): boolean => {
if (!pattern || !comment) return false;
const titleLower = comment?.title?.toLowerCase() || '';
const contentLower = comment?.content?.toLowerCase() || '';
const textToMatch = titleLower + ' ' + contentLower;
return matchesPattern(textToMatch, pattern);
};
+66 -11
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { Comment } from '@plebbit/plebbit-react-hooks';
import { commentMatchesPattern } from '../lib/utils/pattern-utils';
interface FilterItem {
text: string;
@@ -34,6 +35,7 @@ interface CatalogFiltersStore {
searchText: string;
setSearchFilter: (text: string) => void;
clearSearchFilter: () => void;
resetCountsForCurrentSubplebbit: () => void;
}
const useCatalogFiltersStore = create(
@@ -137,10 +139,39 @@ const useCatalogFiltersStore = create(
hide: item.hide ?? true,
top: item.top ?? false,
}));
// Compare new filter items with existing ones to detect pattern changes
const existingItems = get().filterItems;
const updatedItems = nonEmptyItems.map((newItem) => {
// Try to find matching existing item by index or text
const existingItem = existingItems.find((item) => item.text === newItem.text);
// If we found a matching item, keep its counts
if (existingItem) {
return {
...newItem,
count: existingItem.count,
filteredCids: existingItem.filteredCids,
subplebbitCounts: existingItem.subplebbitCounts,
subplebbitFilteredCids: existingItem.subplebbitFilteredCids,
};
}
// If pattern has changed or it's a new filter, reset all counts
return {
...newItem,
count: 0,
filteredCids: new Set<string>(),
subplebbitCounts: new Map<string, number>(),
subplebbitFilteredCids: new Map<string, Set<string>>(),
};
});
set({
filterItems: nonEmptyItems,
filterItems: updatedItems,
filteredCids: new Set<string>(),
});
get().recalcFilteredCount();
get().updateFilter();
},
@@ -154,11 +185,7 @@ const useCatalogFiltersStore = create(
// Apply search filter
if (state.searchText.trim() !== '') {
const searchPattern = state.searchText.toLowerCase();
const title = comment?.title?.toLowerCase() || '';
const content = comment?.content?.toLowerCase() || '';
if (!title.includes(searchPattern) && !content.includes(searchPattern)) {
if (!commentMatchesPattern(comment, state.searchText)) {
return false;
}
}
@@ -170,11 +197,7 @@ const useCatalogFiltersStore = create(
for (let i = 0; i < filterItems.length; i++) {
const item = filterItems[i];
if (item.enabled && item.text.trim() !== '') {
const pattern = item.text.toLowerCase();
const title = comment?.title?.toLowerCase() || '';
const content = comment?.content?.toLowerCase() || '';
if (title.includes(pattern) || content.includes(pattern)) {
if (commentMatchesPattern(comment, item.text)) {
// If we have a current subplebbit and this is a match, increment the count
if (currentSubplebbit && comment.subplebbitAddress === currentSubplebbit) {
// We need to use a timeout to avoid modifying state during a state update
@@ -272,6 +295,38 @@ const useCatalogFiltersStore = create(
return filteredCount;
},
resetCountsForCurrentSubplebbit: () => {
const currentSubplebbit = get().currentSubplebbitAddress;
if (!currentSubplebbit) return;
set((state) => {
const updatedFilterItems = state.filterItems.map((item) => {
const newItem = { ...item };
// Ensure maps are properly initialized
if (!newItem.subplebbitCounts || !(newItem.subplebbitCounts instanceof Map)) {
newItem.subplebbitCounts = new Map();
}
if (!newItem.subplebbitFilteredCids || !(newItem.subplebbitFilteredCids instanceof Map)) {
newItem.subplebbitFilteredCids = new Map();
}
// Reset counts for current subplebbit
newItem.subplebbitCounts.set(currentSubplebbit, 0);
newItem.subplebbitFilteredCids.set(currentSubplebbit, new Set<string>());
return newItem;
});
return {
filterItems: updatedFilterItems,
filteredCount: 0,
};
});
// Trigger filter reapplication to start counting again
get().updateFilter();
},
}),
{
name: 'catalog-filters-storage',
+6 -3
View File
@@ -50,6 +50,7 @@
--button-desktop-text-color: #00e;
--button-desktop-text-color-hover: red;
--close-button-background-image: url("/assets/buttons/cross-red.png");
--open-help-button-background-image: url("/assets/buttons/help-red.png");
/* horizontal rule */
--hr-border: none;
@@ -263,6 +264,7 @@
--button-desktop-text-color: #34345c;
--button-desktop-text-color-hover: #d00;
--close-button-background-image: url("/assets/buttons/cross-blue.png");
--open-help-button-background-image: url("/assets/buttons/help-blue.png");
/* footer */
--footer-item-background-color-desktop: #eef2ff;
@@ -479,6 +481,7 @@
--button-desktop-text-color-hover: red;
--button-text-decoration: underline;
--close-button-background-image: url("/assets/buttons/cross-red.png");
--open-help-button-background-image: url("/assets/buttons/help-red.png");
/* footer */
--footer-item-background-color-desktop: #fed;
@@ -657,7 +660,7 @@
--button-text-decoration: underline;
--button-desktop-text-color-hover: #d00;
--close-button-background-image: url("/assets/buttons/cross-blue.png");
--open-help-button-background-image: url("/assets/buttons/help-blue.png");
/* footer */
--footer-item-background-color-desktop: #eef2ff;
@@ -848,7 +851,7 @@
--button-desktop-text-color: #81a2be;
--button-desktop-text-color-hover: #5f89ac;
--close-button-background-image: url("/assets/buttons/cross-dark.png");
--open-help-button-background-image: url("/assets/buttons/help-dark.png");
/* filter for inputs */
--filter80: brightness(85%);
@@ -1067,7 +1070,7 @@
--button-desktop-text-color: #f60;
--button-desktop-text-color-hover: #f30;
--close-button-background-image: url("/assets/buttons/cross-photon.png");
--open-help-button-background-image: url("/assets/buttons/help-photon.png");
/* horizontal rule */
--hr-border: none;
--hr-border-top: 1px solid #ddd;
+5 -16
View File
@@ -18,6 +18,7 @@ import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
import CatalogRow from '../../components/catalog-row';
import LoadingEllipsis from '../../components/loading-ellipsis';
import styles from './catalog.module.css';
import { commentMatchesPattern } from '../../lib/utils/pattern-utils';
const lastVirtuosoStates: { [key: string]: StateSnapshot } = {};
@@ -39,15 +40,12 @@ const createContentFilter = (
if (enabledFilters.length === 0) return true;
const titleLower = comment?.title?.toLowerCase() || '';
const contentLower = comment?.content?.toLowerCase() || '';
// Check if any enabled filter matches the content
for (let i = 0; i < enabledFilters.length; i++) {
const item = enabledFilters[i];
const pattern = item.text.toLowerCase();
const pattern = item.text;
if (titleLower.includes(pattern) || contentLower.includes(pattern)) {
if (commentMatchesPattern(comment, pattern)) {
// Find the original filter index to increment count
const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled);
if (filterIndex !== -1) {
@@ -102,12 +100,7 @@ const createCombinedFilter = (
const searchFilter = {
filter: (comment: Comment) => {
if (!searchText.trim()) return true;
const titleLower = comment?.title?.toLowerCase() || '';
const contentLower = comment?.content?.toLowerCase() || '';
const searchPattern = searchText.toLowerCase();
return titleLower.includes(searchPattern) || contentLower.includes(searchPattern);
return commentMatchesPattern(comment, searchText);
},
key: searchText ? `search-filter-${searchText}` : 'no-search-filter',
};
@@ -410,13 +403,9 @@ const Catalog = () => {
combinedFeed.forEach((comment) => {
if (!comment) return;
const titleLower = comment?.title?.toLowerCase() || '';
const contentLower = comment?.content?.toLowerCase() || '';
let isTop = false;
for (const filter of enabledTopFilters) {
const pattern = filter.text.toLowerCase();
if (titleLower.includes(pattern) || contentLower.includes(pattern)) {
if (commentMatchesPattern(comment, filter.text)) {
isTop = true;
break;
}