diff --git a/package.json b/package.json index 84d0096a..12552d55 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "dependencies": { "@capacitor/app": "6.0.1", "@floating-ui/react": "0.26.1", - "@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#6d35eb3b4dc84f8fdcbb46d95e3b1d78f1750b5f", + "@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#54cf36c50dd31f5641a9475f5566b7b29b3873c7", "@react-spring/web": "9.7.5", "@testing-library/jest-dom": "5.14.1", "@testing-library/react": "13.0.0", diff --git a/public/assets/banners/banner-63.jpg b/public/assets/banners/banner-63.jpg new file mode 100644 index 00000000..c5abbabe Binary files /dev/null and b/public/assets/banners/banner-63.jpg differ diff --git a/public/assets/buttons/question-blue.png b/public/assets/buttons/help-blue.png similarity index 100% rename from public/assets/buttons/question-blue.png rename to public/assets/buttons/help-blue.png diff --git a/public/assets/buttons/question-dark.png b/public/assets/buttons/help-dark.png similarity index 100% rename from public/assets/buttons/question-dark.png rename to public/assets/buttons/help-dark.png diff --git a/public/assets/buttons/question-photon.png b/public/assets/buttons/help-photon.png similarity index 100% rename from public/assets/buttons/question-photon.png rename to public/assets/buttons/help-photon.png diff --git a/public/assets/buttons/question-red.png b/public/assets/buttons/help-red.png similarity index 100% rename from public/assets/buttons/question-red.png rename to public/assets/buttons/help-red.png diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index ebce90c7..113e9c0e 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "تم إزالة هذا الموضوع بواسطة مشرفي المنتدى", "this_reply_was_deleted": "تم حذف هذه الرد من قبل مؤلفه", "this_reply_was_removed": "تمت إزالة هذه الرد من قبل مشرفي المنتدى", - "not_mod_of_any_board": "أنت لست مشرفًا في أي لوحة." + "not_mod_of_any_board": "أنت لست مشرفًا في أي لوحة.", + "filters_and_highlights": "الفلاتر والإبرازات", + "search_results_for": "نتائج البحث عن", + "help": "مساعدة", + "filter_and_highlights_help": "مساعدة الفلاتر والإبرازات", + "filter_and_highlights": "التصفية والتسليط الضوء" } \ No newline at end of file diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index a4451490..16fef571 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "এই থ্রেডটি বোর্ড moderators দ্বারা মুছে ফেলা হয়েছে", "this_reply_was_deleted": "এই প্রতিক্রিয়া তার লেখক দ্বারা মুছে ফেলা হয়েছে", "this_reply_was_removed": "এই প্রতিক্রিয়া বোর্ড পরিচালকদের দ্বারা সরানো হয়েছে", - "not_mod_of_any_board": "আপনি কোনও বোর্ডের মড নন।" + "not_mod_of_any_board": "আপনি কোনও বোর্ডের মড নন।", + "filters_and_highlights": "ফিল্টার এবং হাইলাইট", + "search_results_for": "অনুসন্ধান ফলাফল", + "help": "সাহায্য", + "filter_and_highlights_help": "ফিল্টার এবং হাইলাইট সাহায্য", + "filter_and_highlights": "ফিল্টার এবং হাইলাইট" } \ No newline at end of file diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 4b1cecc8..70fabc09 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -228,7 +228,7 @@ "pending": "Čeká na schválení", "failed": "Nepodařilo se", "add": "Přidat", - "pattern": "Vzor", + "pattern": "vzor", "enable": "Povolit", "order": "Pořadí", "text": "Text", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Toto vlákno bylo odstraněno moderátory fóra", "this_reply_was_deleted": "Tato odpověď byla smazána autorem", "this_reply_was_removed": "Tato odpověď byla odstraněna moderátory fóra", - "not_mod_of_any_board": "Nejste moderátor žádné desky." + "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", + "help": "Pomoc", + "filter_and_highlights_help": "Pomoc s filtry a zvýrazněním", + "filter_and_highlights": "Filtry a zvýraznění" } \ No newline at end of file diff --git a/public/translations/da/default.json b/public/translations/da/default.json index dfaab88e..daaa9a56 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -228,7 +228,7 @@ "pending": "Afventer", "failed": "Mislykkedes", "add": "Tilføj", - "pattern": "Mønster", + "pattern": "mønster", "enable": "Aktiver", "order": "Rækkefølge", "text": "Tekst", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Denne tråd blev fjernet af boardmoderatorer", "this_reply_was_deleted": "Dette svar blev slettet af forfatteren", "this_reply_was_removed": "Dette svar blev fjernet af forummoderatorer", - "not_mod_of_any_board": "Du er ikke moderator for nogen tavler." + "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", + "help": "Hjælp", + "filter_and_highlights_help": "Filtre og fremhævelser Hjælp", + "filter_and_highlights": "Filtre og fremhævninger" } \ No newline at end of file diff --git a/public/translations/de/default.json b/public/translations/de/default.json index d2b1eab4..59b755c2 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "Dieser Thread wurde von den Moderatoren des Boards entfernt", "this_reply_was_deleted": "Diese Antwort wurde vom Autor gelöscht", "this_reply_was_removed": "Diese Antwort wurde von den Forenmoderatoren entfernt", - "not_mod_of_any_board": "Du bist kein Moderator eines Boards." + "not_mod_of_any_board": "Du bist kein Moderator eines Boards.", + "filters_and_highlights": "Filter & Highlights", + "search_results_for": "Suchergebnisse für", + "help": "Hilfe", + "filter_and_highlights_help": "Filter- und Markierungs-Hilfe", + "filter_and_highlights": "Filter und Hervorhebungen" } \ No newline at end of file diff --git a/public/translations/el/default.json b/public/translations/el/default.json index fcf0fb0e..2929b622 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -228,7 +228,7 @@ "pending": "Σε εκκρεμότητα", "failed": "Απέτυχε", "add": "Προσθήκη", - "pattern": "Μοτίβο", + "pattern": "μοτίβο", "enable": "Ενεργοποιήστε", "order": "Σειρά", "text": "Κείμενο", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Αυτή η συζήτηση αφαιρέθηκε από τους διαχειριστές του φόρουμ", "this_reply_was_deleted": "Αυτή η απάντηση διαγράφηκε από τον συγγραφέα της", "this_reply_was_removed": "Η απάντηση αυτή αφαιρέθηκε από τους διαχειριστές του φόρουμ", - "not_mod_of_any_board": "Δεν είστε διαχειριστής σε καμία κοινότητα." + "not_mod_of_any_board": "Δεν είστε διαχειριστής σε καμία κοινότητα.", + "filters_and_highlights": "Φίλτρα και επισημάνσεις", + "search_results_for": "Αποτελέσματα αναζήτησης για", + "help": "Βοήθεια", + "filter_and_highlights_help": "Βοήθεια φίλτρων και επισήμανσης", + "filter_and_highlights": "Φίλτρα και Επισημάνσεις" } \ No newline at end of file diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 84d51c92..0c9ee101 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -228,7 +228,7 @@ "pending": "Pending", "failed": "Failed", "add": "Add", - "pattern": "Pattern", + "pattern": "pattern", "enable": "Enable", "order": "Order", "text": "Text", @@ -291,5 +291,10 @@ "this_thread_was_removed": "This thread was removed by board moderators", "this_reply_was_deleted": "This reply was deleted by its author", "this_reply_was_removed": "This reply was removed by board moderators", - "not_mod_of_any_board": "You are not a mod of any board." + "not_mod_of_any_board": "You are not a mod of any board.", + "filters_and_highlights": "Filters & Highlights", + "search_results_for": "Search results for", + "help": "Help", + "filter_and_highlights_help": "Filters & Highlights Help", + "filter_and_highlights": "Filters & Highlights" } \ No newline at end of file diff --git a/public/translations/es/default.json b/public/translations/es/default.json index 49807a11..9d1732fa 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -228,7 +228,7 @@ "pending": "Pendiente", "failed": "Fallido", "add": "Añadir", - "pattern": "Patrón", + "pattern": "patrón", "enable": "Habilitar", "order": "Orden", "text": "Texto", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Este hilo fue eliminado por los moderadores del foro", "this_reply_was_deleted": "Esta respuesta fue eliminada por su autor", "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." + "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", + "help": "Ayuda", + "filter_and_highlights_help": "Ayuda de filtros y resaltes", + "filter_and_highlights": "Filtros y resaltados" } \ No newline at end of file diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index b9b8f812..704609a8 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "این تاپیک توسط مدیران انجمن حذف شد", "this_reply_was_deleted": "این پاسخ توسط نویسنده‌اش حذف شد", "this_reply_was_removed": "این پاسخ توسط مدیران انجمن حذف شد", - "not_mod_of_any_board": "شما هیچ تابلو مدیر نیستید." + "not_mod_of_any_board": "شما هیچ تابلو مدیر نیستید.", + "filters_and_highlights": "فیلترها و هایلایت‌ها", + "search_results_for": "نتایج جستجو برای", + "help": "کمک", + "filter_and_highlights_help": "کمک فیلترها و برجسته‌ها", + "filter_and_highlights": "فیلترها و برجسته‌ها" } \ No newline at end of file diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index 2cba44fa..f95e9ee9 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -228,7 +228,7 @@ "pending": "Odottaa", "failed": "Epäonnistui", "add": "Lisää", - "pattern": "Malli", + "pattern": "malli", "enable": "Ota käyttöön", "order": "Järjestys", "text": "Teksti", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Tämä ketju poistettiin foorumin moderaattoreiden toimesta", "this_reply_was_deleted": "Tämä vastaus poistettiin sen kirjoittajalta", "this_reply_was_removed": "Tämä vastaus poistettiin foorumin moderaattoreiden toimesta", - "not_mod_of_any_board": "Et ole moderaattori missään taulussa." + "not_mod_of_any_board": "Et ole moderaattori missään taulussa.", + "filters_and_highlights": "Suodattimet ja korostukset", + "search_results_for": "Hakutulokset hakusanalle", + "help": "Apua", + "filter_and_highlights_help": "Suodattimet ja korostukset -apu", + "filter_and_highlights": "Suodattimet ja korostukset" } \ No newline at end of file diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index 1fde0af6..fe1679f5 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -228,7 +228,7 @@ "pending": "Naghihintay", "failed": "Nabigo", "add": "Idagdag", - "pattern": "Pattern", + "pattern": "pattern", "enable": "I-enable", "order": "Kaayusan", "text": "Teksto", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Inalis ang thread na ito ng mga moderator ng board", "this_reply_was_deleted": "Ang tugon na ito ay tinanggal ng may-akda nito", "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." + "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", + "help": "Tulong", + "filter_and_highlights_help": "Tulong sa mga filter at highlight", + "filter_and_highlights": "Mga filter at highlight" } \ No newline at end of file diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index 890413c5..d987ed39 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -228,7 +228,7 @@ "pending": "En attente", "failed": "Échoué", "add": "Ajouter", - "pattern": "Modèle", + "pattern": "motif", "enable": "Activer", "order": "Ordre", "text": "Texte", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Ce sujet a été supprimé par les modérateurs du forum", "this_reply_was_deleted": "Cette réponse a été supprimée par son auteur", "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." + "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", + "help": "Aide", + "filter_and_highlights_help": "Aide sur les filtres et surlignages", + "filter_and_highlights": "Filtres et surlignages" } \ No newline at end of file diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 117f53bb..728a16d2 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -228,7 +228,7 @@ "pending": "ממתין", "failed": "נכשל", "add": "הוסף", - "pattern": "תבנית", + "pattern": "דפוס", "enable": "הפעל", "order": "סדר", "text": "טקסט", @@ -291,5 +291,10 @@ "this_thread_was_removed": "השרשור הזה הוסר על ידי מנהלי הפורום", "this_reply_was_deleted": "תשובה זו נמחקה על ידי המחבר שלה", "this_reply_was_removed": "תשובה זו הוסרה על ידי מנהלי הפורום", - "not_mod_of_any_board": "אתה לא מוד בכל לוח." + "not_mod_of_any_board": "אתה לא מוד בכל לוח.", + "filters_and_highlights": "סינונים והדגשות", + "search_results_for": "תוצאות חיפוש עבור", + "help": "עזרה", + "filter_and_highlights_help": "עזרה במסננים והדגשות", + "filter_and_highlights": "מסננים והדגשות" } \ No newline at end of file diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index bdd422a4..e9ca1f34 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "यह थ्रेड बोर्ड मॉडरेटर्स द्वारा हटा दिया गया था", "this_reply_was_deleted": "इस उत्तर को इसके लेखक द्वारा हटा दिया गया था", "this_reply_was_removed": "यह उत्तर बोर्ड मॉडरेटर द्वारा हटा दिया गया था", - "not_mod_of_any_board": "आप किसी बोर्ड के मॉड नहीं हैं।" + "not_mod_of_any_board": "आप किसी बोर्ड के मॉड नहीं हैं।", + "filters_and_highlights": "फ़िल्टर और हाइलाइट्स", + "search_results_for": "के लिए खोज परिणाम", + "help": "मदद", + "filter_and_highlights_help": "फ़िल्टर और हाइलाइट्स सहायता", + "filter_and_highlights": "फ़िल्टर और हाइलाइट्स" } \ No newline at end of file diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index 7affabb6..33fe9f0f 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -228,7 +228,7 @@ "pending": "Függőben", "failed": "Sikertelen", "add": "Hozzáad", - "pattern": "Minta", + "pattern": "minta", "enable": "Engedélyez", "order": "Rendelés", "text": "Szöveg", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Ezt a témát a fórum moderátorai eltávolították", "this_reply_was_deleted": "Ez a válasz törölve lett a szerzője által", "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." + "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", + "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" } \ No newline at end of file diff --git a/public/translations/id/default.json b/public/translations/id/default.json index 65240c4c..a07da699 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -228,7 +228,7 @@ "pending": "Tertunda", "failed": "Gagal", "add": "Tambahkan", - "pattern": "Pola", + "pattern": "pola", "enable": "Aktifkan", "order": "Urutan", "text": "Teks", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Thread ini dihapus oleh moderator papan", "this_reply_was_deleted": "Balasan ini dihapus oleh penulisnya", "this_reply_was_removed": "Balasan ini dihapus oleh moderator papan", - "not_mod_of_any_board": "Anda bukan mod di papan manapun." + "not_mod_of_any_board": "Anda bukan mod di papan manapun.", + "filters_and_highlights": "Filter dan Sorotan", + "search_results_for": "Hasil pencarian untuk", + "help": "Bantuan", + "filter_and_highlights_help": "Bantuan Filter dan Sorotan", + "filter_and_highlights": "Filter dan Sorotan" } \ No newline at end of file diff --git a/public/translations/it/default.json b/public/translations/it/default.json index 4352987e..d38e0082 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -228,7 +228,7 @@ "pending": "In attesa", "failed": "Fallito", "add": "Aggiungi", - "pattern": "Modello", + "pattern": "modello", "enable": "Abilita", "order": "Ordine", "text": "Testo", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Questo thread è stato rimosso dai moderatori del board", "this_reply_was_deleted": "Questa risposta è stata eliminata dal suo autore", "this_reply_was_removed": "Questa risposta è stata rimossa dai moderatori del forum", - "not_mod_of_any_board": "Non sei un mod di nessuna board." + "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", + "help": "Aiuto", + "filter_and_highlights_help": "Aiuto su Filtri e Evidenziazioni", + "filter_and_highlights": "Filtri e Evidenziazioni" } \ No newline at end of file diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index 1b225aea..86c89a10 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "このスレッドはボードのモデレーターによって削除されました", "this_reply_was_deleted": "この返信はその著者によって削除されました", "this_reply_was_removed": "この返信は掲示板のモデレーターによって削除されました", - "not_mod_of_any_board": "あなたはどのボードのモデレーターでもありません。" + "not_mod_of_any_board": "あなたはどのボードのモデレーターでもありません。", + "filters_and_highlights": "フィルターとハイライト", + "search_results_for": "検索結果", + "help": "ヘルプ", + "filter_and_highlights_help": "フィルターとハイライトのヘルプ", + "filter_and_highlights": "フィルターとハイライト" } \ No newline at end of file diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index cabe38ac..d0c5093c 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "이 스레드는 보드 관리자에 의해 제거되었습니다", "this_reply_was_deleted": "이 답글은 작성자에 의해 삭제되었습니다", "this_reply_was_removed": "이 답글은 게시판 관리자에 의해 삭제되었습니다", - "not_mod_of_any_board": "당신은 어떤 게시판의 관리자도 아닙니다." + "not_mod_of_any_board": "당신은 어떤 게시판의 관리자도 아닙니다.", + "filters_and_highlights": "필터 및 하이라이트", + "search_results_for": "검색 결과", + "help": "도움말", + "filter_and_highlights_help": "필터 및 하이라이트 도움말", + "filter_and_highlights": "필터 및 하이라이트" } \ No newline at end of file diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index e1fb115b..43a896bd 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "हे थ्रेड बोर्ड मध्यमाने काढले गेले आहे", "this_reply_was_deleted": "हा प्रतिसाद त्याच्या लेखकाने हटवला आहे", "this_reply_was_removed": "या उत्तराला बोर्ड मॉडरेटरने काढले आहे", - "not_mod_of_any_board": "तुम्ही कोणत्याही बोर्डचे मॉड नाहीत." + "not_mod_of_any_board": "तुम्ही कोणत्याही बोर्डचे मॉड नाहीत.", + "filters_and_highlights": "फिल्टर आणि हायलाइट्स", + "search_results_for": "सर्च परिणाम", + "help": "मदत", + "filter_and_highlights_help": "फिल्टर आणि हायलाइट्स सहाय्य", + "filter_and_highlights": "फिल्टर आणि हायलाइट्स" } \ No newline at end of file diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index 3a8ceb86..fc9a2b5d 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -228,7 +228,7 @@ "pending": "In afwachting", "failed": "Mislukt", "add": "Toevoegen", - "pattern": "Patroon", + "pattern": "patroon", "enable": "Inschakelen", "order": "Volgorde", "text": "Tekst", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Deze thread werd verwijderd door boardmoderators", "this_reply_was_deleted": "Dit antwoord werd verwijderd door de auteur", "this_reply_was_removed": "Dit antwoord is verwijderd door de forum moderators", - "not_mod_of_any_board": "Je bent geen mod van een bord." + "not_mod_of_any_board": "Je bent geen mod van een bord.", + "filters_and_highlights": "Filters en markeringen", + "search_results_for": "Zoekresultaten voor", + "help": "Help", + "filter_and_highlights_help": "Filters en Markeringen Help", + "filter_and_highlights": "Filters en markeringen" } \ No newline at end of file diff --git a/public/translations/no/default.json b/public/translations/no/default.json index a9f98e2f..6d78cf80 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -228,7 +228,7 @@ "pending": "Venter", "failed": "Feilet", "add": "Legg til", - "pattern": "Mønster", + "pattern": "mønster", "enable": "Aktiver", "order": "Rekkefølge", "text": "Tekst", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Denne tråden ble fjernet av boardmoderatorer", "this_reply_was_deleted": "Dette svaret ble slettet av forfatteren", "this_reply_was_removed": "Dette svaret ble fjernet av forummoderatorer", - "not_mod_of_any_board": "Du er ikke moderator på noen brett." + "not_mod_of_any_board": "Du er ikke moderator på noen brett.", + "filters_and_highlights": "Filtre og fremhevinger", + "search_results_for": "Søkeresultater for", + "help": "Hjelp", + "filter_and_highlights_help": "Filtre og fremhevinger Hjelp", + "filter_and_highlights": "Filtre og uthevinger" } \ No newline at end of file diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index d3d35c42..688d198c 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -228,7 +228,7 @@ "pending": "Oczekujące", "failed": "Nieudane", "add": "Dodaj", - "pattern": "Wzór", + "pattern": "wzór", "enable": "Włącz", "order": "Kolejność", "text": "Tekst", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Wątek został usunięty przez moderatorów forum", "this_reply_was_deleted": "Ta odpowiedź została usunięta przez autora", "this_reply_was_removed": "Ta odpowiedź została usunięta przez moderatorów forum", - "not_mod_of_any_board": "Nie jesteś moderatorem żadnej tablicy." + "not_mod_of_any_board": "Nie jesteś moderatorem żadnej tablicy.", + "filters_and_highlights": "Filtry i wyróżnienia", + "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" } \ No newline at end of file diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index dc6c0523..fb52e2f8 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -228,7 +228,7 @@ "pending": "Pendente", "failed": "Falhou", "add": "Adicionar", - "pattern": "Padrão", + "pattern": "padrão", "enable": "Ativar", "order": "Ordem", "text": "Texto", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Este tópico foi removido pelos moderadores do fórum", "this_reply_was_deleted": "Esta resposta foi excluída pelo autor", "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." + "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", + "help": "Ajuda", + "filter_and_highlights_help": "Ajuda de Filtros e Realces", + "filter_and_highlights": "Filtros e Destaques" } \ No newline at end of file diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index 456f1e4e..57181d73 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -228,7 +228,7 @@ "pending": "În așteptare", "failed": "Eșuat", "add": "Adaugă", - "pattern": "Model", + "pattern": "model", "enable": "Activare", "order": "Ordine", "text": "Text", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Acest thread a fost eliminat de moderatorii board-ului", "this_reply_was_deleted": "Acest răspuns a fost șters de autorul său", "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." + "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", + "help": "Ajutor", + "filter_and_highlights_help": "Ajutor pentru filtre și evidențieri", + "filter_and_highlights": "Filtre și evidențieri" } \ No newline at end of file diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index fcec4b16..443d2ae5 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -228,7 +228,7 @@ "pending": "В ожидании", "failed": "Не удалось", "add": "Добавить", - "pattern": "Шаблон", + "pattern": "узор", "enable": "Включить", "order": "Порядок", "text": "Текст", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Эта тема была удалена модераторами форума", "this_reply_was_deleted": "Этот ответ был удален автором", "this_reply_was_removed": "Этот ответ был удален модераторами форума", - "not_mod_of_any_board": "Вы не модератор ни одной доски." + "not_mod_of_any_board": "Вы не модератор ни одной доски.", + "filters_and_highlights": "Фильтры и выделения", + "search_results_for": "Результаты поиска для", + "help": "Помощь", + "filter_and_highlights_help": "Помощь с фильтрами и выделениями", + "filter_and_highlights": "Фильтры и выделения" } \ No newline at end of file diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index 3dfd4993..a3cc9707 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -228,7 +228,7 @@ "pending": "Në pritje", "failed": "Dështuar", "add": "Shto", - "pattern": "Model", + "pattern": "model", "enable": "Aktivizo", "order": "Rendit", "text": "Tekst", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Ky thread u hoq nga moderatorët e bordit", "this_reply_was_deleted": "Ky përgjigje u fshi nga autori i saj", "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." + "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", + "help": "Ndihmë", + "filter_and_highlights_help": "Ndihma për Filtro & Thekso", + "filter_and_highlights": "Filtra dhe thekson" } \ No newline at end of file diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index f2f34930..63883358 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -228,7 +228,7 @@ "pending": "Väntande", "failed": "Misslyckades", "add": "Lägg till", - "pattern": "Mönster", + "pattern": "mönster", "enable": "Aktivera", "order": "Ordning", "text": "Text", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Denna tråd togs bort av forumets moderatorer", "this_reply_was_deleted": "Detta svar togs bort av författaren", "this_reply_was_removed": "Detta svar togs bort av forumets moderatorer", - "not_mod_of_any_board": "Du är inte moderator på någon tavla." + "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", + "help": "Hjälp", + "filter_and_highlights_help": "Filter och markeringar Hjälp", + "filter_and_highlights": "Filter och markeringar" } \ No newline at end of file diff --git a/public/translations/te/default.json b/public/translations/te/default.json index b8cf78ec..ffb84a90 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -228,7 +228,7 @@ "pending": "అనుమతించబడాలి", "failed": "విజయవంతం కాలేదు", "add": "చేర్చండి", - "pattern": "ప్యాటర్న్", + "pattern": "పాటర్న్", "enable": "చేర్చు", "order": "ఆర్డర్", "text": "పాఠం", @@ -291,5 +291,10 @@ "this_thread_was_removed": "ఈ థ్రెడ్ బోర్డ్ మోడరేటర్ల ద్వారా తీసివేయబడింది", "this_reply_was_deleted": "ఈ స్పందనను దాని రచయిత తొలగించారు", "this_reply_was_removed": "ఈ స్పందన బోర్డ్ మోడరేటర్లచే తొలగించబడింది", - "not_mod_of_any_board": "మీరు ఎటువంటి బోర్డుకు మోడ్ కంటే లేరు." + "not_mod_of_any_board": "మీరు ఎటువంటి బోర్డుకు మోడ్ కంటే లేరు.", + "filters_and_highlights": "ఫిల్టర్లు మరియు హైలైట్‌లు", + "search_results_for": "శోధన ఫలితాలు", + "help": "సహాయం", + "filter_and_highlights_help": "ఫిల్టర్లు మరియు హైలైట్ సహాయం", + "filter_and_highlights": "ఫిల్టర్లు మరియు హైలైట్స్" } \ No newline at end of file diff --git a/public/translations/th/default.json b/public/translations/th/default.json index a91107c7..885880a5 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "กระทู้นี้ถูกลบโดยผู้ดูแลบอร์ด", "this_reply_was_deleted": "การตอบกลับนี้ถูกลบโดยผู้แต่ง", "this_reply_was_removed": "คำตอบนี้ถูกลบโดยผู้ดูแลบอร์ด", - "not_mod_of_any_board": "คุณไม่ใช่ผู้ดูแลบอร์ดใดๆ" + "not_mod_of_any_board": "คุณไม่ใช่ผู้ดูแลบอร์ดใดๆ", + "filters_and_highlights": "ตัวกรองและไฮไลท์", + "search_results_for": "ผลการค้นหาสำหรับ", + "help": "ช่วยเหลือ", + "filter_and_highlights_help": "ตัวกรองและการเน้นความช่วยเหลือ", + "filter_and_highlights": "ตัวกรองและไฮไลท์" } \ No newline at end of file diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index 1b338343..b74ec005 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -228,7 +228,7 @@ "pending": "Beklemede", "failed": "Başarısız", "add": "Ekle", - "pattern": "Desen", + "pattern": "desen", "enable": "Etkinleştir", "order": "Sıra", "text": "Metin", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Bu konu, board moderatörleri tarafından kaldırıldı", "this_reply_was_deleted": "Bu yanıt, yazar tarafından silindi", "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." + "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", + "help": "Yardım", + "filter_and_highlights_help": "Filtreler ve Vurgular Yardımı", + "filter_and_highlights": "Filtreler ve Vurgular" } \ No newline at end of file diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index f2849a7b..81c3feff 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -228,7 +228,7 @@ "pending": "Очікує", "failed": "Не вдалося", "add": "Додати", - "pattern": "Шаблон", + "pattern": "шаблон", "enable": "Увімкнути", "order": "Порядок", "text": "Текст", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Цю тему було видалено модераторами форуму", "this_reply_was_deleted": "Цю відповідь було видалено її автором", "this_reply_was_removed": "Цю відповідь було видалено модераторами форуму", - "not_mod_of_any_board": "Ви не модератор жодної дошки." + "not_mod_of_any_board": "Ви не модератор жодної дошки.", + "filters_and_highlights": "Фільтри та виділення", + "search_results_for": "Результати пошуку для", + "help": "Допомога", + "filter_and_highlights_help": "Допомога з фільтрами та підсвічуваннями", + "filter_and_highlights": "Фільтри та виділення" } \ No newline at end of file diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index 8053751e..03240bd8 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -228,7 +228,7 @@ "pending": "منتظر", "failed": "ناکام", "add": "شامل کریں", - "pattern": "نمونہ", + "pattern": "پیٹرن", "enable": "فعال کریں", "order": "ترتیب", "text": "متن", @@ -291,5 +291,10 @@ "this_thread_was_removed": "اس تھریڈ کو بورڈ کے موڈریٹرز نے ہٹا دیا ہے", "this_reply_was_deleted": "اس جواب کو اس کے مصنف نے حذف کر دیا تھا", "this_reply_was_removed": "اس جواب کو بورڈ کے منتظمین نے حذف کر دیا ہے", - "not_mod_of_any_board": "آپ کسی بورڈ کے موڈ نہیں ہیں۔" + "not_mod_of_any_board": "آپ کسی بورڈ کے موڈ نہیں ہیں۔", + "filters_and_highlights": "فلٹرز اور ہائیلائٹس", + "search_results_for": "تلاش کے نتائج", + "help": "مدد", + "filter_and_highlights_help": "فلٹرز اور ہائی لائٹس کی مدد", + "filter_and_highlights": "فلٹرز اور ہائلाइटس" } \ No newline at end of file diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index 89d4e137..9c5cb55d 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -228,7 +228,7 @@ "pending": "Đang chờ", "failed": "Thất bại", "add": "Thêm", - "pattern": "Mẫu", + "pattern": "mẫu", "enable": "Kích hoạt", "order": "Thứ tự", "text": "Văn bản", @@ -291,5 +291,10 @@ "this_thread_was_removed": "Chủ đề này đã bị các quản trị viên diễn đàn xóa", "this_reply_was_deleted": "Câu trả lời này đã bị tác giả của nó xóa", "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." + "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", + "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" } \ No newline at end of file diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index e595957e..b8fc65bc 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -291,5 +291,10 @@ "this_thread_was_removed": "此帖子已被论坛版主删除", "this_reply_was_deleted": "该回复已被其作者删除", "this_reply_was_removed": "该回复已被版主删除", - "not_mod_of_any_board": "您不是任何板块的管理员。" + "not_mod_of_any_board": "您不是任何板块的管理员。", + "filters_and_highlights": "筛选器和高亮", + "search_results_for": "搜索结果", + "help": "帮助", + "filter_and_highlights_help": "过滤器和高亮帮助", + "filter_and_highlights": "筛选器与高亮" } \ No newline at end of file diff --git a/src/app.tsx b/src/app.tsx index 3843ee3b..b3810f16 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -55,8 +55,9 @@ const BoardLayout = () => { {isMobile ? (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( <> - +
+ ) : (subplebbitAddress || isInAllView || isInSubscriptionsView || pendingPost?.subplebbitAddress) && ( diff --git a/src/components/board-buttons/board-buttons.module.css b/src/components/board-buttons/board-buttons.module.css index 43068ba4..59d754e4 100644 --- a/src/components/board-buttons/board-buttons.module.css +++ b/src/components/board-buttons/board-buttons.module.css @@ -51,7 +51,6 @@ } .desktopBoardButtons { - padding-right: 5px; text-transform: capitalize; } diff --git a/src/components/board-buttons/board-buttons.tsx b/src/components/board-buttons/board-buttons.tsx index 5efa3ec5..61ce220c 100644 --- a/src/components/board-buttons/board-buttons.tsx +++ b/src/components/board-buttons/board-buttons.tsx @@ -1,19 +1,21 @@ import { useTranslation } from 'react-i18next'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; -import { useAccountComment, useComment, useSubplebbit, 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 useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import { isAllView, isCatalogView, isDescriptionView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; +import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useSortingStore from '../../stores/use-sorting-store'; -import useTimeFilter from '../../hooks/use-time-filter'; -import CatalogFilters from '../../views/catalog/catalog-filters/'; -import styles from './board-buttons.module.css'; -import Tooltip from '../tooltip'; import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; -import _ from 'lodash'; import useIsMobile from '../../hooks/use-is-mobile'; -import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; -import { useEffect } from 'react'; +import useTimeFilter from '../../hooks/use-time-filter'; +import CatalogFilters from '../catalog-filters'; +import CatalogSearch from '../catalog-search'; +import Tooltip from '../tooltip'; +import styles from './board-buttons.module.css'; +import _ from 'lodash'; interface BoardButtonsProps { address?: string | undefined; @@ -244,13 +246,7 @@ export const MobileBoardButtons = () => { const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; - const { filteredCount, resetFilteredCount } = useCatalogFiltersStore(); - - useEffect(() => { - if (subplebbitAddress) { - resetFilteredCount(); - } - }, [subplebbitAddress, resetFilteredCount]); + const { filteredCount, searchText } = useCatalogFiltersStore(); return (
@@ -273,11 +269,19 @@ export const MobileBoardButtons = () => { )} {!(isInAllView || isInSubscriptionsView) && } - {isInCatalogView && filteredCount > 0 && ( + {isInCatalogView && searchText ? ( {' '} - — {t('filtered_threads')}: {filteredCount} + — {t('search_results_for')}: {searchText} + ) : ( + isInCatalogView && + filteredCount > 0 && ( + + {' '} + — {t('filtered_threads')}: {filteredCount} + + ) )} {isInCatalogView && ( <> @@ -287,7 +291,7 @@ export const MobileBoardButtons = () => {
- +
@@ -304,8 +308,9 @@ const PostPageStats = () => { const location = useLocation(); const isInDescriptionView = isDescriptionView(location.pathname, params); - const comment = useComment({ commentCid: params?.commentCid }); - const subplebbit = useSubplebbit({ subplebbitAddress: params?.subplebbitAddress }); + const comment = useSubplebbitsPagesStore((state) => state.comments[params?.commentCid as string]); + const subplebbit = useSubplebbitsStore((state) => state.subplebbits[params?.subplebbitAddress as string]); + const descriptionReplyCount = location?.pathname.startsWith('/p/all/') ? 0 : subplebbit?.rules?.length > 0 ? 1 : 0; const { closed, pinned, replyCount } = comment || {}; const linkCount = useCountLinksInReplies(comment); @@ -334,13 +339,7 @@ export const DesktopBoardButtons = () => { const isInPostView = isPostPageView(location.pathname, params); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); - const { filteredCount, resetFilteredCount } = useCatalogFiltersStore(); - - useEffect(() => { - if (subplebbitAddress) { - resetFilteredCount(); - } - }, [subplebbitAddress, resetFilteredCount]); + const { filteredCount, searchText } = useCatalogFiltersStore(); return ( <> @@ -373,11 +372,19 @@ export const DesktopBoardButtons = () => { )} [] - {isInCatalogView && filteredCount > 0 && ( + {isInCatalogView && searchText ? ( {' '} - — {t('filtered_threads')}: {filteredCount} + — {t('search_results_for')}: {searchText} + ) : ( + isInCatalogView && + filteredCount > 0 && ( + + {' '} + — {t('filtered_threads')}: {filteredCount} + + ) )} {isInCatalogView && ( @@ -390,17 +397,16 @@ export const DesktopBoardButtons = () => { {(isInAllView || isInSubscriptionsView) && ( )} - {isInCatalogView && ( - <> - [ - ] - - )}{' '} {!(isInAllView || isInSubscriptionsView) && ( <> [ ] + )}{' '} + {isInCatalogView && ( + <> + [] + )} diff --git a/src/components/board-header/board-header.tsx b/src/components/board-header/board-header.tsx index 071511d4..1aa0150d 100644 --- a/src/components/board-header/board-header.tsx +++ b/src/components/board-header/board-header.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useLocation, useParams } from 'react-router-dom'; -import { useAccountComment, useSubplebbit } from '@plebbit/plebbit-react-hooks'; +import { useAccountComment } from '@plebbit/plebbit-react-hooks'; +import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils'; import styles from './board-header.module.css'; import { useMultisubMetadata } from '../../hooks/use-default-subplebbits'; @@ -9,7 +10,7 @@ import useIsSubplebbitOffline from '../../hooks/use-is-subplebbit-offline'; import { shouldShowSnow } from '../../lib/snow'; import Tooltip from '../tooltip'; -const totalBanners = 62; +const totalBanners = 63; const ImageBanner = () => { const [imagePath] = useState(() => { @@ -29,7 +30,8 @@ const BoardHeader = () => { const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; - const subplebbit = useSubplebbit({ subplebbitAddress }); + const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]); + const { address, shortAddress } = subplebbit || {}; const multisubMetadata = useMultisubMetadata(); diff --git a/src/views/catalog/catalog-filters/catalog-filters.module.css b/src/components/catalog-filters/catalog-filters.module.css similarity index 71% rename from src/views/catalog/catalog-filters/catalog-filters.module.css rename to src/components/catalog-filters/catalog-filters.module.css index e5039403..c8609028 100644 --- a/src/views/catalog/catalog-filters/catalog-filters.module.css +++ b/src/components/catalog-filters/catalog-filters.module.css @@ -33,22 +33,43 @@ .modal { top: 60px; - left: 50%; + left: calc(50% + 5px); transform: translateX(-50%); - width: 340px; + width: 380px; height: auto; max-height: 80%; overflow-y: auto; position: fixed; z-index: 5; - padding: 2px; + padding: 2px 5px 3px 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); +} + +@media (max-width: 640px) { + .modal { + padding-bottom: 0; + } +} + +.filtersProtipModal { + top: 50px; + width: 570px; + left: 50%; + padding: 2px 10px 5px 10px; +} + +@media (max-width: 614px) { + .filtersProtipModal { + width: 90vw; + } +} + +@media (max-width: 420px) { + .modal { + width: 90vw; + } } .header { @@ -76,6 +97,20 @@ 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; } .filters { @@ -102,16 +137,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 { @@ -142,7 +173,13 @@ outline: none; border: var(--reply-modal-field-input-border, revert); font-size: 11px; - width: 200px; + width: 150px; +} + +@media (max-width: 640px) { + .filtersTable input[type='text'] { + font-size: 16px !important; + } } .filtersTable input[type='text']:focus { @@ -162,5 +199,16 @@ } .saveButton { - float: right; + position: absolute; + right: 5px; +} +@media (max-width: 640px) { + .addButton, .saveButton { + margin-bottom: 4px !important; + } +} + +.filterHits { + font-size: 12px !important; + text-transform: none; } diff --git a/src/components/catalog-filters/catalog-filters.tsx b/src/components/catalog-filters/catalog-filters.tsx new file mode 100644 index 00000000..c2eed963 --- /dev/null +++ b/src/components/catalog-filters/catalog-filters.tsx @@ -0,0 +1,234 @@ +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 FiltersProtip from './filters-protip'; +import HighlightColorPicker from './highlight-color-picker'; +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) => ({ + ...item, + hide: item.hide ?? true, + top: item.top ?? false, + color: item.color ?? '', + })), + ); + + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + // Update local items when store changes + useEffect(() => { + setLocalFilterItems( + filterItems.map((item) => ({ + ...item, + hide: item.hide ?? true, + top: item.top ?? false, + color: item.color ?? '', + })), + ); + }, [filterItems]); + + const handleAddFilter = useCallback(() => { + setLocalFilterItems((prev) => { + const newIndex = prev.length; + setTimeout(() => { + inputRefs.current[newIndex]?.focus(); + }, 0); + return [ + ...prev, + { + text: '', + enabled: true, + count: 0, + filteredCids: new Set(), + subplebbitCounts: new Map(), + subplebbitFilteredCids: new Map>(), + hide: true, + top: false, + color: '', + }, + ]; + }); + }, []); + + const handleSave = useCallback(() => { + const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== ''); + + saveAndApplyFilters(nonEmptyFilters); + + useCatalogFiltersStore.getState().resetCountsForCurrentSubplebbit(); + + if (resetFeed) { + resetFeed(); + } + + onSave(); + }, [saveAndApplyFilters, localFilterItems, onSave, resetFeed]); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleSave(); + } + }, + [handleSave], + ); + + useEffect(() => { + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [handleKeyDown]); + + const updateLocalFilterItem = useCallback((index: number, item: any) => { + setLocalFilterItems((prev) => prev.map((f, i) => (i === index ? item : f))); + }, []); + + const removeLocalFilterItem = useCallback((index: number) => { + setLocalFilterItems((prev) => prev.filter((_, i) => i !== index)); + }, []); + + const moveLocalFilterItemUp = useCallback((index: number) => { + if (index === 0) return; + setLocalFilterItems((prev) => { + const newItems = [...prev]; + [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]]; + return newItems; + }); + }, []); + + return ( + + + + + + + + + + + + + + {localFilterItems.map((item, index) => ( + + + + + + + + + + + ))} + + + + + + +
orderonpatterncolorhidetopdel
+ moveLocalFilterItemUp(index)}> + ↑ + + + updateLocalFilterItem(index, { ...item, enabled: e.target.checked })} + /> + + updateLocalFilterItem(index, { ...item, text: e.target.value })} + ref={(el) => (inputRefs.current[index] = el)} + /> + + + + updateLocalFilterItem(index, { ...item, hide: e.target.checked })} /> + + updateLocalFilterItem(index, { ...item, top: e.target.checked })} /> + + removeLocalFilterItem(index)}> + × + + + {currentSubplebbitAddress && item.subplebbitFilteredCids?.has(currentSubplebbitAddress) && `x${item.subplebbitCounts?.get(currentSubplebbitAddress) ?? 0}`} +
+ + +
+ ); +}; + +const FiltersModal = ({ closeModal }: { closeModal: () => void }) => { + const { t } = useTranslation(); + const [showHelp, setShowHelp] = useState(false); + const openHelp = () => setShowHelp(true); + const closeHelp = () => setShowHelp(false); + + useEffect(() => { + const onEscapeKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + if (showHelp) { + closeHelp(); + } else { + closeModal(); + } + } + }; + document.addEventListener('keydown', onEscapeKey); + return () => document.removeEventListener('keydown', onEscapeKey); + }, [closeModal, showHelp]); + + return ( + <> +
+
+
+ {showHelp ? t('filter_and_highlights_help') : t('filter_and_highlights')} + {!showHelp && } + +
+ {showHelp ? : } +
+ + ); +}; + +const CatalogFilters = () => { + const { t } = useTranslation(); + const [showModal, setShowModal] = useState(false); + + const closeModal = useCallback(() => { + setShowModal(false); + }, []); + + return ( + <> + setShowModal(true)}> + {t('filters')} + + {showModal && } + + ); +}; + +export default CatalogFilters; diff --git a/src/components/catalog-filters/filters-protip/filters-protip.module.css b/src/components/catalog-filters/filters-protip/filters-protip.module.css new file mode 100644 index 00000000..96de3b06 --- /dev/null +++ b/src/components/catalog-filters/filters-protip/filters-protip.module.css @@ -0,0 +1,34 @@ +.filtersProtip { + text-transform: none; + text-align: left; +} + +.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; +} \ No newline at end of file diff --git a/src/components/catalog-filters/filters-protip/filters-protip.tsx b/src/components/catalog-filters/filters-protip/filters-protip.tsx new file mode 100644 index 00000000..5ae12370 --- /dev/null +++ b/src/components/catalog-filters/filters-protip/filters-protip.tsx @@ -0,0 +1,104 @@ +import styles from './filters-protip.module.css'; + +const FiltersProtip = () => { + return ( +
+

Patterns

+
    +
  • + Matching whole words: +
  • +
  • + feel — will match "feel" but not "feeling". This search is case-insensitive. +
  • +
+
    +
  • + AND operator: +
  • +
  • + feel girlfriend — will match "feel" AND "girlfriend" in any order. +
  • +
+
    +
  • + OR operator: +
  • +
  • + feel|girlfriend — will match "feel" OR "girlfriend". +
  • +
+
    +
  • + Mixing both operators: +
  • +
  • + girlfriend|boyfriend feel — matches "feel" AND "girlfriend", or "feel" AND "boyfriend". +
  • +
+
    +
  • + Exact match search: +
  • +
  • + "that feel when" — place double quotes around the pattern to search for an exact string. +
  • +
+
    +
  • + Wildcards: +
  • +
  • + feel* — matches expressions such as "feel", "feels", "feeling", "feeler", etc… +
  • +
  • + idolm*ster — this can match "idolmaster" or "idolm@ster", etc… +
  • +
+
    + Filtering by display name or user ID +
  • + Prefix the pattern with # to search by user ID: #GPPr4CQSRCt8 +
  • +
  • + Prefix the pattern with ## to search by name: ##Anonymous +
  • +
  • + To filter by role: #!#owner, #!#admin, #!#moderator (or #!#mod) +
  • +
+
    + It is also possible to filter by regular expression: +
  • + /^(?=.*detachable)(?=.*hats).*$/i — AND operator. +
  • +
  • + /^(?!.*touhou).*$/i — NOT operator. +
  • +
  • + {'/^>/'} — threads starting with a quote ({'">"'} character as an html entity). +
  • +
  • + /^$/ — threads with no text. +
  • +
+

Controls

+
    +
  • + On — enables or disables the filter. +
  • +
  • + Color — highlights matched threads with the specified color. +
  • +
  • + Hide — hides matched threads. +
  • +
  • + Top — moves the filter to the top of the feed. +
  • +
+
+ ); +}; + +export default FiltersProtip; diff --git a/src/components/catalog-filters/filters-protip/index.ts b/src/components/catalog-filters/filters-protip/index.ts new file mode 100644 index 00000000..6e661867 --- /dev/null +++ b/src/components/catalog-filters/filters-protip/index.ts @@ -0,0 +1 @@ +export { default } from './filters-protip'; diff --git a/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.module.css b/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.module.css new file mode 100644 index 00000000..38bdf5c1 --- /dev/null +++ b/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.module.css @@ -0,0 +1,110 @@ +.colorClickbox { + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + background-color: #fff; + border: var(--reply-modal-field-input-border, revert); + text-decoration: none; + color: #000; + position: relative; + cursor: pointer; +} + +.colorPickerModal { + padding: 4px; + z-index: 1000; + font-size: 14px; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); + background-color: var(--settings-modal-background-color); +} + +.colorPickerModal table { + border-spacing: 1px; + margin-left: auto; + margin-right: auto; +} + +.colorPickerModal table td { + padding: 2px; + text-align: center; + font-size: 10pt; + margin: 0; +} + +.colorOption { + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + display: block; + text-align: center; + border: 1px solid #aaa; + text-decoration: none; + color: #000; + white-space: nowrap; + user-select: none; + cursor: pointer; +} + +.middleTxt input[type="text"] { + width: 45px; + margin: 0 2px; + margin-right: 5px; + padding: 1px; + outline: none; + border: var(--reply-modal-field-input-border, revert); + font-size: 11px; + background-color: default; + vertical-align: top; +} + +.middleTxt input[type="text"]:focus { + border: var(--reply-modal-field-input-border-focus, revert); +} + +@media (max-width: 640px) { + .middleTxt input[type="text"] { + font-size: 16px !important; + } +} + +.middleTxt .colorClickbox { + width: 16px; + height: 16px; + line-height: 16px; + font-size: 10px; + text-align: center; + border: 1px solid #aaa; + text-decoration: none; + color: #000; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.2); + margin: auto; + cursor: pointer; + float: right; + background-color: transparent !important; +} + +.colorPickerModal tfoot .button { + color: var(--button-desktop-text-color); + cursor: pointer; + margin: 0 2px; +} + +.colorPickerModal tfoot .button:hover { + color: var(--button-desktop-text-color-hover); +} + +.colorPickerModal tfoot .button::before{ + content: '['; + color: var(--body-font-color); +} + +.colorPickerModal tfoot .button::after{ + content: ']'; + color: var(--body-font-color); +} diff --git a/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.tsx b/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.tsx new file mode 100644 index 00000000..10f11f05 --- /dev/null +++ b/src/components/catalog-filters/highlight-color-picker/highlight-color-picker.tsx @@ -0,0 +1,145 @@ +import { useState, useRef } from 'react'; +import styles from './highlight-color-picker.module.css'; +import { autoUpdate, FloatingFocusManager, FloatingPortal, offset, shift, useClick, useDismiss, useFloating, useId, useInteractions, useRole } from '@floating-ui/react'; + +interface ColorPickerProps { + item: any; + index: number; + updateLocalFilterItem: (index: number, item: any) => void; + localFilterItems: any[]; +} + +const HighlightColorPicker = ({ item, index, updateLocalFilterItem, localFilterItems }: ColorPickerProps) => { + const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); + const [customColor, setCustomColor] = useState(''); + + const inputRef = useRef(null); + + const { refs, floatingStyles, context } = useFloating({ + placement: 'left-start', + open: isColorPickerOpen, + onOpenChange: setIsColorPickerOpen, + middleware: [offset(10), shift()], + whileElementsMounted: autoUpdate, + }); + + const click = useClick(context); + const dismiss = useDismiss(context); + const role = useRole(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([click, dismiss, role]); + + const headingId = useId(); + + const colorOptions = ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00', '#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB', '#00A550', '#007FFF', '#AF0A0F', '#B5BD68']; + + const colorRows = [colorOptions.slice(0, 4), colorOptions.slice(4, 8), colorOptions.slice(8, 12)]; + + const setItemColor = (color: string) => { + updateLocalFilterItem(index, { ...localFilterItems[index], color }); + setIsColorPickerOpen(false); + }; + + const handleCustomColorChange = (e: React.ChangeEvent) => { + const newColor = e.target.value; + setCustomColor(newColor); + }; + + const applyCustomColor = () => { + if (customColor) { + setItemColor(customColor); + } + }; + + const clearColor = () => { + setItemColor(''); + }; + + return ( + <> + + {!item.color && '∕'} + + + {isColorPickerOpen && ( + +
{ + e.preventDefault(); + e.stopPropagation(); + setIsColorPickerOpen(false); + }} + /> + +
e.stopPropagation()} + > + + + {colorRows.map((row, rowIndex) => ( + + {row.map((color, colorIndex) => ( + + ))} + + ))} + + + + + + + + + + + + +
+ setItemColor(color)} /> +
Custom
+ + +
+ setIsColorPickerOpen(false)}> + Close + + + Clear + +
+
+
+ + )} + + ); +}; + +export default HighlightColorPicker; diff --git a/src/components/catalog-filters/highlight-color-picker/index.ts b/src/components/catalog-filters/highlight-color-picker/index.ts new file mode 100644 index 00000000..63571df7 --- /dev/null +++ b/src/components/catalog-filters/highlight-color-picker/index.ts @@ -0,0 +1 @@ +export { default } from './highlight-color-picker'; diff --git a/src/views/catalog/catalog-filters/index.ts b/src/components/catalog-filters/index.ts similarity index 100% rename from src/views/catalog/catalog-filters/index.ts rename to src/components/catalog-filters/index.ts diff --git a/src/components/catalog-row/catalog-row.module.css b/src/components/catalog-row/catalog-row.module.css index df3843f8..748a5262 100644 --- a/src/components/catalog-row/catalog-row.module.css +++ b/src/components/catalog-row/catalog-row.module.css @@ -13,6 +13,8 @@ padding: 10px 0 3px 0; vertical-align: top; display: inline-block; + box-sizing: border-box; + margin: 2px; } .postContent { @@ -69,14 +71,14 @@ background-color: var(--media-thumbnail-background-color); width: var(--width); height: var(--height); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); display: inline-flex; justify-content: center; align-items: center; - clip-path: inset(-5px); /* direct media could change its dimensions after being published, so we need to clip the overflow. We can't use overflow: hidden because it will hide the box-shadow */ + overflow: hidden; } .post img, .post video, .post audio { - box-shadow: 0 0 5px rgba(0, 0, 0, 0.25); max-width: var(--maxWidth); max-height: var(--maxHeight); } diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index dcf6d556..8ffc4f63 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -2,34 +2,36 @@ import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { Link, useLocation, useParams } from 'react-router-dom'; -import { Comment } from '@plebbit/plebbit-react-hooks'; import { useFloating, offset, size, autoUpdate, Placement } from '@floating-ui/react'; +import { Comment } from '@plebbit/plebbit-react-hooks'; +import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; +import { shouldShowSnow } from '../../lib/snow'; import { getHasThumbnail } from '../../lib/utils/media-utils'; import { getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { isAllView, isSubscriptionsView } from '../../lib/utils/view-utils'; -import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; +import useCatalogFiltersStore from '../../stores/use-catalog-filters-store'; import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useEditCommentPrivileges from '../../hooks/use-author-privileges'; +import { useCommentMediaInfo } from '../../hooks/use-comment-media-info'; import useCountLinksInReplies from '../../hooks/use-count-links-in-replies'; import useFetchGifFirstFrame from '../../hooks/use-fetch-gif-first-frame'; import useHide from '../../hooks/use-hide'; import useWindowWidth from '../../hooks/use-window-width'; +import useReplies from '../../hooks/use-replies'; +import { ContentPreview } from '../../views/home/popular-threads-box'; import PostMenuDesktop from '../post-desktop/post-menu-desktop'; import styles from './catalog-row.module.css'; import _ from 'lodash'; -import { ContentPreview } from '../../views/home/popular-threads-box'; -import { useCommentMediaInfo } from '../../hooks/use-comment-media-info'; -import { shouldShowSnow } from '../../lib/snow'; -import useReplies from '../../hooks/use-replies'; interface CatalogPostMediaProps { + cid: string; commentMediaInfo: any; isOutOfFeed?: boolean; linkWidth?: number; linkHeight?: number; } -export const CatalogPostMedia = ({ commentMediaInfo, isOutOfFeed, linkWidth, linkHeight }: CatalogPostMediaProps) => { +export const CatalogPostMedia = ({ cid, commentMediaInfo, isOutOfFeed, linkWidth, linkHeight }: CatalogPostMediaProps) => { const { patternThumbnailUrl, thumbnail, type, url } = commentMediaInfo || {}; const iframeThumbnail = patternThumbnailUrl || thumbnail; const gifFrameUrl = useFetchGifFirstFrame(type === 'gif' ? url : undefined); @@ -89,8 +91,16 @@ export const CatalogPostMedia = ({ commentMediaInfo, isOutOfFeed, linkWidth, lin thumbnailComponent =
@@ -306,7 +306,7 @@ const PostMedia = ({ commentMediaInfo, hasThumbnail, isDescription, isRules, spo ); }; -const Reply = ({ postReplyCount, reply, roles, replies }: PostProps) => { +const Reply = ({ postReplyCount, reply, roles }: PostProps) => { let post = reply; // handle pending mod or author edit const { editedComment } = useEditedComment({ comment: reply }); @@ -327,7 +327,7 @@ const Reply = ({ postReplyCount, reply, roles, replies }: PostProps) => {
{'>>'}
- + {link && !hidden && !(deleted || removed) && isValidURL(link) && ( { parentCid={parentCid} /> )} - {!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && } + {!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && }
); }; -const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, replies: threadReplies }: PostProps) => { +const PostDesktop = ({ post, roles, showAllReplies, showReplies = true }: PostProps) => { const { t } = useTranslation(); const { author, cid, content, deleted, link, linkHeight, linkWidth, pinned, postCid, removed, spoiler, state, subplebbitAddress, thumbnailUrl, parentCid } = post || {}; const { isDescription, isRules } = post || {}; // custom properties, not from api @@ -360,8 +360,7 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, replies: const { hidden, unhide, hide } = useHide({ cid }); const isHidden = hidden && !isInPostPageView; - const commentReplies = useReplies(post); - const replies = threadReplies || commentReplies; + const replies = useReplies(post); const visiblelinksCount = useCountLinksInReplies(post, 5); const totalLinksCount = useCountLinksInReplies(post); const replyCount = replies?.length; @@ -417,9 +416,9 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, replies: parentCid={parentCid} /> )} - + {!isHidden && !content && !(deleted || removed) &&
} - {!isHidden && } + {!isHidden && }
{!isHidden && !isDescription && !isRules && !isInPendingPostView && (replyCount > 5 || (pinned && repliesCount > 0)) && !isInPostPageView && ( @@ -433,11 +432,16 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, replies: }} + components={{ 1: }} values={{ repliesCount, linksCount }} /> ) : ( - }} values={{ repliesCount }} /> + }} + values={{ repliesCount }} + /> )} )} @@ -448,7 +452,7 @@ const PostDesktop = ({ post, roles, showAllReplies, showReplies = true, replies: showReplies && (showAllReplies || showOmittedReplies[cid] ? replies : replies.slice(-5)).map((reply, index) => (
- +
))} {isDescription && subplebbit?.rules && subplebbit?.rules.length > 0 && ( diff --git a/src/components/post-form/post-form.module.css b/src/components/post-form/post-form.module.css index 7dca9193..649c43c1 100644 --- a/src/components/post-form/post-form.module.css +++ b/src/components/post-form/post-form.module.css @@ -15,8 +15,7 @@ .postFormMobile { text-align: center; - margin-top: -2px; - padding-bottom: 7px; + margin-top: -3px; } .closed { diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index e02b00a8..1d107805 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import { Comment, setAccount, useAccount, useAccountComment, useComment, useEditedComment, useSubplebbit } from '@plebbit/plebbit-react-hooks'; +import { Comment, setAccount, useAccount, useAccountComment, useEditedComment } from '@plebbit/plebbit-react-hooks'; +import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; +import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils'; import { formatMarkdown } from '../../lib/utils/post-utils'; import { isValidURL } from '../../lib/utils/url-utils'; @@ -56,7 +58,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid: const defaultSubplebbitAddresses = useDefaultSubplebbitAddresses(); const { anonMode, getNewSigner, getExistingSigner } = useAnonMode(postCid); - const comment = useComment({ commentCid: postCid }); + const comment = useSubplebbitsPagesStore((state) => state.comments[postCid]); const address = comment?.author?.address; const [lengthError, setLengthError] = useState(null); @@ -427,9 +429,10 @@ const PostForm = () => { const isInPostView = isPostPageView(location.pathname, params); const isInRulesView = isRulesView(location.pathname, params); const isInAllView = isAllView(location.pathname); - const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); + const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); - const post = useComment({ commentCid: useParams().commentCid }); + const commentCid = params?.commentCid; + const post = useSubplebbitsPagesStore((state) => state.comments[commentCid as string]); let comment: Comment = post; // handle pending mod or author edit const { editedComment } = useEditedComment({ comment }); @@ -444,7 +447,7 @@ const PostForm = () => { const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; - const subplebbit = useSubplebbit({ subplebbitAddress }); + const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]); const { isOffline, isOnlineStatusLoading, offlineTitle } = useIsSubplebbitOffline(subplebbit); return ( diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 6541a6d0..95c09aa1 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -233,7 +233,7 @@ const PostMediaContent = ({ post, link }: { post: any; link: string }) => { ); }; -const ReplyBacklinks = ({ post, replies: threadReplies }: PostProps) => { +const ReplyBacklinks = ({ post }: PostProps) => { const { cid, parentCid } = post || {}; const replies = useReplies(post); @@ -246,14 +246,14 @@ const ReplyBacklinks = ({ post, replies: threadReplies }: PostProps) => { (reply: Comment, index: number) => reply?.parentCid === cid && reply?.cid && - !(reply?.deleted || reply?.removed) && , + !(reply?.deleted || reply?.removed) && , )}
) ); }; -const Reply = ({ postReplyCount, reply, replies, roles }: PostProps) => { +const Reply = ({ postReplyCount, reply, roles }: PostProps) => { let post = reply; // handle pending mod or author edit const { editedComment } = useEditedComment({ comment: reply }); @@ -274,7 +274,7 @@ const Reply = ({ postReplyCount, reply, replies, roles }: PostProps) => { data-post-cid={postCid} > - {!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && } + {!hidden && (!(removed || deleted) || ((removed || deleted) && reason)) && } @@ -282,7 +282,7 @@ const Reply = ({ postReplyCount, reply, replies, roles }: PostProps) => { ); }; -const PostMobile = ({ post, roles, showAllReplies, showReplies = true, replies: threadReplies }: PostProps) => { +const PostMobile = ({ post, roles, showAllReplies, showReplies = true }: PostProps) => { const { t } = useTranslation(); const { author, cid, pinned, postCid, replyCount, state, subplebbitAddress } = post || {}; const { isDescription, isRules } = post || {}; // custom properties, not from api @@ -292,8 +292,7 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true, replies: const isInPendingPostView = isPendingPostView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params); const linksCount = useCountLinksInReplies(post); - const commentReplies = useReplies(post); - const replies = threadReplies || commentReplies; + const replies = useReplies(post); const isInPostPageView = isPostPageView(location.pathname, params); const { hidden, unhide } = useHide({ cid }); @@ -339,7 +338,7 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true, replies: > {shouldShowSnow() && } - + {!isInPostView && !isInPendingPostView && showReplies && (
@@ -362,7 +361,7 @@ const PostMobile = ({ post, roles, showAllReplies, showReplies = true, replies: showReplies && (showAllReplies ? replies : replies.slice(-5)).map((reply, index) => (
- +
))} {showRules && ( diff --git a/src/components/reply-modal/reply-modal.tsx b/src/components/reply-modal/reply-modal.tsx index 79a9cb35..5f29ab27 100644 --- a/src/components/reply-modal/reply-modal.tsx +++ b/src/components/reply-modal/reply-modal.tsx @@ -1,7 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation, useParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { setAccount, useAccount, useComment, useSubplebbit } from '@plebbit/plebbit-react-hooks'; +import { setAccount, useAccount } from '@plebbit/plebbit-react-hooks'; +import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; +import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import Plebbit from '@plebbit/plebbit-js/dist/browser/index.js'; import { formatMarkdown } from '../../lib/utils/post-utils'; import { getFormattedTimeAgo } from '../../lib/utils/time-utils'; @@ -45,7 +47,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s const { selectedText } = useSelectedTextStore(); const { anonMode, getNewSigner, getExistingSigner } = useAnonMode(postCid); - const comment = useComment({ commentCid: postCid }); + const comment = useSubplebbitsPagesStore((state) => state.comments[postCid]); const address = comment?.author?.address; const getAnonAddressForReply = useCallback(async () => { @@ -189,7 +191,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, postCid, scrollY, s const location = useLocation(); const isInAllView = isAllView(location.pathname); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); - const subplebbit = useSubplebbit({ subplebbitAddress }); + const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]); const { updatedAt } = subplebbit || {}; const isBoardOffline = subplebbit?.updatedAt && subplebbit.updatedAt < Date.now() / 1000 - 60 * 60; const offlineAlert = updatedAt diff --git a/src/components/reply-quote-preview/reply-quote-preview.tsx b/src/components/reply-quote-preview/reply-quote-preview.tsx index 0d68b05f..c0fa61d1 100644 --- a/src/components/reply-quote-preview/reply-quote-preview.tsx +++ b/src/components/reply-quote-preview/reply-quote-preview.tsx @@ -13,7 +13,6 @@ interface ReplyQuotePreviewProps { backlinkReply?: Comment; isQuotelinkReply?: boolean; quotelinkReply?: Comment; - replies?: Comment[]; } const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => { @@ -54,7 +53,7 @@ const handleQuoteHover = (cid: string, onElementOutOfView: () => void) => { } }; -const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, replies }: ReplyQuotePreviewProps) => { +const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => { const [hoveredCid, setHoveredCid] = useState(null); const [outOfViewCid, setOutOfViewCid] = useState(null); const placementRef = useRef('right'); @@ -145,7 +144,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i outOfViewCid === backlinkReply?.cid && createPortal(
- +
, document.body, )} @@ -174,7 +173,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i outOfViewCid === quotelinkReply?.cid && createPortal(
- +
, document.body, )} @@ -184,7 +183,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink; }; -const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, replies }: ReplyQuotePreviewProps) => { +const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => { const [hoveredCid, setHoveredCid] = useState(null); const [outOfViewCid, setOutOfViewCid] = useState(null); @@ -257,7 +256,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is outOfViewCid === backlinkReply?.cid && createPortal(
- +
, document.body, )} @@ -293,7 +292,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is outOfViewCid === quotelinkReply?.cid && createPortal(
- +
, document.body, )} @@ -303,25 +302,13 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is return isBacklinkReply ? replyBacklink : isQuotelinkReply && replyQuotelink; }; -const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, replies }: ReplyQuotePreviewProps) => { +const ReplyQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply }: ReplyQuotePreviewProps) => { const isMobile = useIsMobile(); return isMobile ? ( - + ) : ( - + ); }; diff --git a/src/components/settings-modal/account-settings/account-settings.tsx b/src/components/settings-modal/account-settings/account-settings.tsx index 7ffb4e4d..3d360b58 100644 --- a/src/components/settings-modal/account-settings/account-settings.tsx +++ b/src/components/settings-modal/account-settings/account-settings.tsx @@ -132,7 +132,6 @@ const AccountSettings = () => { fileInput.type = 'file'; fileInput.accept = '.json'; - // Handle file selection fileInput.onchange = async (event) => { try { const files = (event.target as HTMLInputElement).files; @@ -141,7 +140,6 @@ const AccountSettings = () => { } const file = files[0]; - // Read the file content const reader = new FileReader(); reader.onload = async (e) => { try { @@ -149,20 +147,40 @@ const AccountSettings = () => { if (typeof fileContent !== 'string') { throw new Error('File content is not a string.'); } - const newAccount = JSON.parse(fileContent); - await importAccount(fileContent); - // Store the imported account's address - if (newAccount.account?.author?.address) { - localStorage.setItem('importedAccountAddress', newAccount.account.author.address); + const accountData = JSON.parse(fileContent); + + // Add subplebbit addresses to subscriptions if they exist + if (accountData.account?.subplebbits) { + const subplebbitAddresses = Object.keys(accountData.account.subplebbits); + + if (!accountData.account.subscriptions) { + accountData.account.subscriptions = []; + } + + const uniqueSubscriptions = [...accountData.account.subscriptions]; + + for (const address of subplebbitAddresses) { + if (!uniqueSubscriptions.includes(address)) { + uniqueSubscriptions.push(address); + } + } + + accountData.account.subscriptions = uniqueSubscriptions; } - // Set the new account as active before reloading - if (newAccount.account?.name) { - await setActiveAccount(newAccount.account.name); + const modifiedAccountJson = JSON.stringify(accountData); + await importAccount(modifiedAccountJson); + + if (accountData.account?.author?.address) { + localStorage.setItem('importedAccountAddress', accountData.account.author.address); } - alert(`Imported ${newAccount.account?.name}`); + if (accountData.account?.name) { + await setActiveAccount(accountData.account.name); + } + + alert(`Imported ${accountData.account?.name}`); const currentPath = location.pathname; if (!currentPath.includes('/settings#account-settings')) { @@ -189,7 +207,6 @@ const AccountSettings = () => { } }; - // Trigger file selection dialog fileInput.click(); }; diff --git a/src/components/subplebbit-stats/subplebbit-stats.tsx b/src/components/subplebbit-stats/subplebbit-stats.tsx index 16d9ed9c..de712dff 100644 --- a/src/components/subplebbit-stats/subplebbit-stats.tsx +++ b/src/components/subplebbit-stats/subplebbit-stats.tsx @@ -1,6 +1,8 @@ import { useLocation, useParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { useAccountComment, useComment, useSubplebbit, useSubplebbitStats } from '@plebbit/plebbit-react-hooks'; +import { useAccountComment, useSubplebbitStats } from '@plebbit/plebbit-react-hooks'; +import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; +import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import useSubplebbitStatsVisibilityStore from '../../stores/use-subplebbit-stats-visibility-store'; import { isDescriptionView, isRulesView } from '../../lib/utils/view-utils'; import styles from './subplebbit-stats.module.css'; @@ -12,7 +14,7 @@ const SubplebbitStats = () => { const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const subplebbitAddress = params?.subplebbitAddress || accountComment?.subplebbitAddress; - const subplebbit = useSubplebbit({ subplebbitAddress }); + const subplebbit = useSubplebbitsStore((state) => state.subplebbits[subplebbitAddress]); const { address, createdAt } = subplebbit || {}; let stats = useSubplebbitStats({ subplebbitAddress: address }); @@ -23,7 +25,7 @@ const SubplebbitStats = () => { const isInDescriptionView = isDescriptionView(location.pathname, params); const isInRulesView = isRulesView(location.pathname, params); - const comment = useComment({ commentCid: params?.commentCid }); + const comment = useSubplebbitsPagesStore((state) => state.comments[params?.commentCid as string]); const { deleted, locked, removed } = comment || {}; const hideStats = deleted || locked || removed || isInDescriptionView || isInRulesView; diff --git a/src/components/topbar/topbar.tsx b/src/components/topbar/topbar.tsx index d148e1d4..64a8b9a0 100644 --- a/src/components/topbar/topbar.tsx +++ b/src/components/topbar/topbar.tsx @@ -144,7 +144,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => { const params = useParams(); const isInAllView = isAllView(location.pathname); const isInCatalogView = isCatalogView(location.pathname, params); - const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); + const isInSubscriptionsView = isSubscriptionsView(location.pathname, params); const selectValue = isInAllView ? 'all' : isInSubscriptionsView ? 'subscriptions' : subplebbitAddress; const { accountSubplebbits } = useAccountSubplebbits(); @@ -160,7 +160,7 @@ const TopBarMobile = ({ subplebbitAddress }: { subplebbitAddress: string }) => { const subplebbitAddress = address?.includes('.') ? address : Plebbit.getShortAddress(address); return ( ); })} diff --git a/src/hooks/use-auto-subscribe.ts b/src/hooks/use-auto-subscribe.ts index 6bb95b7f..2175a674 100644 --- a/src/hooks/use-auto-subscribe.ts +++ b/src/hooks/use-auto-subscribe.ts @@ -5,7 +5,6 @@ import { getAutoSubscribeAddresses, useDefaultSubplebbits } from './use-default- const AUTO_SUBSCRIBE_KEY_PREFIX = 'seedit-auto-subscribe-done-'; -// Keep track of which accounts have been processed globally const processedAccounts = new Set(); export const useAutoSubscribe = () => { @@ -31,7 +30,7 @@ export const useAutoSubscribe = () => { const storageKey = AUTO_SUBSCRIBE_KEY_PREFIX + accountAddress; const hasAutoSubscribed = localStorage.getItem(storageKey); - if (account.subscriptions?.length > 0 || hasAutoSubscribed) { + if (hasAutoSubscribed) { processedAccounts.add(accountAddress); removeCheckingAccount(accountAddress); return; @@ -40,9 +39,19 @@ export const useAutoSubscribe = () => { const autoSubscribeAddresses = getAutoSubscribeAddresses(); if (autoSubscribeAddresses.length) { try { + const currentSubscriptions = account.subscriptions || []; + + const mergedSubscriptions = [...currentSubscriptions]; + + for (const address of autoSubscribeAddresses) { + if (!mergedSubscriptions.includes(address)) { + mergedSubscriptions.push(address); + } + } + await setAccount({ ...account, - subscriptions: autoSubscribeAddresses, + subscriptions: mergedSubscriptions, }); localStorage.setItem(storageKey, 'true'); processedAccounts.add(accountAddress); diff --git a/src/hooks/use-catalog-feed-rows.ts b/src/hooks/use-catalog-feed-rows.ts index bb995bbc..17b2cfb8 100644 --- a/src/hooks/use-catalog-feed-rows.ts +++ b/src/hooks/use-catalog-feed-rows.ts @@ -7,6 +7,7 @@ import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils'; import { isAllView } from '../lib/utils/view-utils'; import { useMultisubMetadata } from './use-default-subplebbits'; import _ from 'lodash'; +import useCatalogFiltersStore from '../stores/use-catalog-filters-store'; const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolean, subplebbit: Subplebbit) => { const { t } = useTranslation(); @@ -19,6 +20,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea const multisub = useMultisubMetadata(); const { accountComments } = useAccountComments(); + const { searchText } = useCatalogFiltersStore(); const feedWithFakePostsOnTop = useMemo(() => { if (!isFeedLoaded) { @@ -64,7 +66,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea } // add subplebbit description and rules as fake posts at the top of the feed - if (description && description.length > 0) { + if (description && description.length > 0 && !searchText) { _feed.unshift({ isDescription: true, subplebbitAddress: address, @@ -79,7 +81,7 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea } // rules are shown in description thread if both are set - if (rules && rules.length > 0 && !description) { + if (rules && rules.length > 0 && !description && !searchText) { _feed.unshift({ isRules: true, subplebbitAddress: address, @@ -93,7 +95,23 @@ const useCatalogFeedRows = (columnCount: number, feed: any, isFeedLoaded: boolea } return _feed; - }, [accountComments, feed, description, rules, address, isFeedLoaded, createdAt, title, shortAddress, avatarUrl, t, isInAllView, multisub, hideThreadsWithoutImages]); + }, [ + accountComments, + feed, + description, + rules, + address, + isFeedLoaded, + createdAt, + title, + shortAddress, + avatarUrl, + t, + isInAllView, + multisub, + hideThreadsWithoutImages, + searchText, + ]); const rows = useMemo(() => { const rows = []; diff --git a/src/index.css b/src/index.css index 3bf6c15f..97e46b19 100644 --- a/src/index.css +++ b/src/index.css @@ -52,6 +52,10 @@ hr { color-scheme: dark; } +[data-floating-ui-portal] { + color-scheme: var(--color-scheme) !important; +} + .yellowOfflineIcon { background-image: url('/assets/icons/offline-blue.png'); } diff --git a/src/lib/utils/pattern-utils.ts b/src/lib/utils/pattern-utils.ts new file mode 100644 index 00000000..b260d162 --- /dev/null +++ b/src/lib/utils/pattern-utils.ts @@ -0,0 +1,261 @@ +import { Comment } from '@plebbit/plebbit-react-hooks'; +import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; + +/** + * 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 user ID matches a pattern + * + * @param comment The comment to check + * @param pattern The pattern to match (without the # prefix) + * @returns True if the user ID matches the pattern, false otherwise + */ +export const userIdMatchesPattern = (comment: Comment, pattern: string): boolean => { + if (!pattern || !comment?.author) return false; + + // Check both address and shortAddress if available + const address = comment.author.address || ''; + const shortAddress = comment.author.shortAddress || ''; + + return address.includes(pattern) || shortAddress.includes(pattern); +}; + +/** + * Checks if a display name matches a pattern + * + * @param comment The comment to check + * @param pattern The pattern to match (without the ## prefix) + * @returns True if the display name matches the pattern, false otherwise + */ +export const displayNameMatchesPattern = (comment: Comment, pattern: string): boolean => { + if (!pattern) return false; + + // Special case for "Anonymous" to match undefined display names + if (pattern.toLowerCase() === 'anonymous') { + return comment?.author?.displayName === undefined; + } + + if (!comment?.author?.displayName) return false; + + return comment.author.displayName.toLowerCase().includes(pattern.toLowerCase()); +}; + +/** + * Checks if a user has a specific role + * + * @param comment The comment to check + * @param role The role to check for (without the #!# prefix) + * @returns True if the user has the specified role, false otherwise + */ +export const userHasRole = (comment: Comment, role: string): boolean => { + if (!role || !comment?.author?.address || !comment?.subplebbitAddress) { + return false; + } + + const subplebbits = useSubplebbitsStore.getState().subplebbits; + const subplebbit = subplebbits[comment.subplebbitAddress]; + + if (!subplebbit?.roles) { + return false; + } + + const userRole = subplebbit.roles[comment.author.address]?.role; + + // Handle different role names (moderator/mod) + if ((role.toLowerCase() === 'moderator' || role.toLowerCase() === 'mod') && userRole === 'moderator') { + return true; + } + + return userRole?.toLowerCase() === role.toLowerCase(); +}; + +/** + * Parses a pattern that may contain special filters and content filters + * + * @param pattern The pattern to parse + * @returns An object containing the parsed special filters and content filters + */ +export const parsePattern = ( + pattern: string, +): { + specialFilters: { type: 'userId' | 'displayName' | 'role'; value: string }[]; + contentFilter: string; +} => { + if (!pattern) return { specialFilters: [], contentFilter: '' }; + + const parts = pattern.split(' '); + const specialFilters: { type: 'userId' | 'displayName' | 'role'; value: string }[] = []; + const contentParts: string[] = []; + + parts.forEach((part) => { + if (part.startsWith('#') && !part.startsWith('##') && !part.startsWith('#!#')) { + specialFilters.push({ type: 'userId', value: part.substring(1) }); + } else if (part.startsWith('##')) { + specialFilters.push({ type: 'displayName', value: part.substring(2) }); + } else if (part.startsWith('#!#')) { + specialFilters.push({ type: 'role', value: part.substring(3) }); + } else { + contentParts.push(part); + } + }); + + return { + specialFilters, + contentFilter: contentParts.join(' '), + }; +}; + +/** + * Checks if a comment matches a pattern in its title or content, + * or if it matches special filters for user ID, display name, or role + * + * @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; + + // Check if the pattern contains spaces, which might indicate combined filters + if (pattern.includes(' ') && (pattern.includes('#') || pattern.includes('##') || pattern.includes('#!#'))) { + // Parse the pattern into special filters and content filter + const { specialFilters, contentFilter } = parsePattern(pattern); + + // If there are special filters, check if the comment matches all of them + if (specialFilters.length > 0) { + const allSpecialFiltersMatch = specialFilters.every((filter) => { + if (filter.type === 'userId') { + return userIdMatchesPattern(comment, filter.value); + } else if (filter.type === 'displayName') { + return displayNameMatchesPattern(comment, filter.value); + } else if (filter.type === 'role') { + return userHasRole(comment, filter.value); + } + return false; + }); + + // If there's also a content filter, check if the comment matches it as well + if (contentFilter) { + return allSpecialFiltersMatch && matchesPattern((comment?.title || '') + ' ' + (comment?.content || ''), contentFilter); + } + + return allSpecialFiltersMatch; + } + } + + // Simple cases for single filters + // Check for user ID filter (starts with #) + if (pattern.startsWith('#') && !pattern.startsWith('##') && !pattern.startsWith('#!#')) { + const userIdPattern = pattern.substring(1); + return userIdMatchesPattern(comment, userIdPattern); + } + + // Check for display name filter (starts with ##) + if (pattern.startsWith('##')) { + const displayNamePattern = pattern.substring(2); + return displayNameMatchesPattern(comment, displayNamePattern); + } + + // Check for role filter (starts with #!#) + if (pattern.startsWith('#!#')) { + const rolePattern = pattern.substring(3); + return userHasRole(comment, rolePattern); + } + + // Regular content matching + const titleLower = comment?.title?.toLowerCase() || ''; + const contentLower = comment?.content?.toLowerCase() || ''; + const textToMatch = titleLower + ' ' + contentLower; + + return matchesPattern(textToMatch, pattern); +}; diff --git a/src/stores/use-catalog-filters-store.ts b/src/stores/use-catalog-filters-store.ts index ef72762f..9d0b88e0 100644 --- a/src/stores/use-catalog-filters-store.ts +++ b/src/stores/use-catalog-filters-store.ts @@ -1,11 +1,18 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { Comment } from '@plebbit/plebbit-react-hooks'; -import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils'; +import { commentMatchesPattern } from '../lib/utils/pattern-utils'; interface FilterItem { text: string; enabled: boolean; + count: number; + filteredCids: Set; + subplebbitCounts: Map; + subplebbitFilteredCids: Map>; + hide: boolean; + top: boolean; + color?: string; } interface CatalogFiltersStore { @@ -20,7 +27,19 @@ interface CatalogFiltersStore { updateFilter: () => void; initializeFilter: () => void; filteredCount: number; - resetFilteredCount: () => void; + filteredCids: Set; + incrementFilterCount: (filterIndex: number, cid: string, subplebbitAddress: string) => void; + recalcFilteredCount: () => void; + currentSubplebbitAddress: string | null; + setCurrentSubplebbitAddress: (address: string | null) => void; + getFilteredCountForCurrentSubplebbit: () => number; + searchText: string; + setSearchFilter: (text: string) => void; + clearSearchFilter: () => void; + resetCountsForCurrentSubplebbit: () => void; + matchedFilters: Map; + setMatchedFilter: (cid: string, color: string) => void; + clearMatchedFilters: () => void; } const useCatalogFiltersStore = create( @@ -28,76 +47,346 @@ const useCatalogFiltersStore = create( (set, get) => ({ showTextOnlyThreads: false, setShowTextOnlyThreads: (value: boolean) => { - set({ showTextOnlyThreads: value, filteredCount: 0 }); + set({ showTextOnlyThreads: value }); get().updateFilter(); }, filterText: '', setFilterText: (value: string) => set({ filterText: value }), filterItems: [], + filteredCount: 0, + filteredCids: new Set(), + currentSubplebbitAddress: null, + matchedFilters: new Map(), + setMatchedFilter: (cid: string, color: string) => { + set((state) => { + const newMatchedFilters = new Map(state.matchedFilters); + if (color) { + newMatchedFilters.set(cid, color); + } else { + newMatchedFilters.delete(cid); + } + return { matchedFilters: newMatchedFilters }; + }); + }, + clearMatchedFilters: () => { + set({ matchedFilters: new Map() }); + }, + setCurrentSubplebbitAddress: (address: string | null) => { + const prevAddress = get().currentSubplebbitAddress; + + if (address !== prevAddress) { + set((state) => { + if (address) { + const updatedFilterItems = state.filterItems.map((item) => { + const newItem = { ...item }; + + // Reset count and filteredCids (global counters) + newItem.count = 0; + newItem.filteredCids = new Set(); + + // Ensure subplebbitCounts is a Map + if (!newItem.subplebbitCounts || !(newItem.subplebbitCounts instanceof Map)) { + newItem.subplebbitCounts = new Map(); + } + + // Ensure subplebbitFilteredCids is a Map + if (!newItem.subplebbitFilteredCids || !(newItem.subplebbitFilteredCids instanceof Map)) { + newItem.subplebbitFilteredCids = new Map(); + } + + // Only initialize for the new address if it doesn't already exist + if (!newItem.subplebbitFilteredCids.has(address)) { + newItem.subplebbitFilteredCids.set(address, new Set()); + } + if (!newItem.subplebbitCounts.has(address)) { + newItem.subplebbitCounts.set(address, 0); + } + + return newItem; + }); + + return { + currentSubplebbitAddress: address, + filterItems: updatedFilterItems, + filteredCount: 0, // This will be recalculated below + }; + } + + return { currentSubplebbitAddress: address }; + }); + + // Recalculate the filtered count for the current subplebbit + get().recalcFilteredCount(); + get().updateFilter(); + } else { + set({ currentSubplebbitAddress: address }); + } + }, + searchText: '', + setSearchFilter: (text: string) => { + set({ searchText: text }); + get().updateFilter(); + }, + clearSearchFilter: () => { + set({ searchText: '' }); + get().updateFilter(); + }, setFilterItems: (items: FilterItem[]) => { - const nonEmptyItems = items.filter((item) => item.text.trim() !== ''); + const nonEmptyItems = items + .filter((item) => item.text.trim() !== '') + .map((item) => ({ + ...item, + count: item.count || 0, + filteredCids: item.filteredCids || new Set(), + subplebbitCounts: item.subplebbitCounts || new Map(), + subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(), + hide: item.hide ?? true, + top: item.top ?? false, + color: item.color || '', + })); set({ filterItems: nonEmptyItems }); + get().recalcFilteredCount(); }, saveAndApplyFilters: (items: FilterItem[]) => { - const nonEmptyItems = items.filter((item) => item.text.trim() !== ''); - set({ filterItems: nonEmptyItems, filteredCount: 0 }); + const nonEmptyItems = items + .filter((item) => item.text.trim() !== '') + .map((item) => ({ + ...item, + count: item.count || 0, + filteredCids: item.filteredCids || new Set(), + subplebbitCounts: item.subplebbitCounts || new Map(), + subplebbitFilteredCids: item.subplebbitFilteredCids || new Map(), + hide: item.hide ?? true, + top: item.top ?? false, + color: item.color || '', + })); + + // 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, + color: newItem.color || '', + }; + } + + // If pattern has changed or it's a new filter, reset all counts + return { + ...newItem, + count: 0, + filteredCids: new Set(), + subplebbitCounts: new Map(), + subplebbitFilteredCids: new Map>(), + color: newItem.color || '', + }; + }); + + // Clear matched filters when saving new filters + get().clearMatchedFilters(); + + set({ + filterItems: updatedItems, + filteredCids: new Set(), + }); + + get().recalcFilteredCount(); get().updateFilter(); }, filter: undefined, updateFilter: () => { - // const filteredCids = new Set(); set((state) => ({ filter: (comment: Comment) => { - const { - showTextOnlyThreads, - // filterItems - } = state; + if (!comment?.cid) return true; - const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {}; + const currentSubplebbit = state.currentSubplebbitAddress; - const hasThumbnail = getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link); - // const title = comment?.title?.toLowerCase() || ''; - // const content = comment?.content?.toLowerCase() || ''; - - // const matchesFilterItems = filterItems - // .filter((item) => item.enabled) - // .some((item) => { - // const text = item.text.toLowerCase(); - // return title.includes(text) || content.includes(text); - // }); - - const shouldShow = showTextOnlyThreads || hasThumbnail; - - if ( - !shouldShow - // || matchesFilterItems - ) { - // if ( - // matchesFilterItems && - // !filteredCids.has(comment.cid)) { - // filteredCids.add(comment.cid); - // set((state) => ({ filteredCount: state.filteredCount + 1 })); - // } - return false; + // Apply search filter + if (state.searchText.trim() !== '') { + if (!commentMatchesPattern(comment, state.searchText)) { + return false; + } } - return true; + // Apply content filters + const { filterItems } = state; + let shouldHide = false; + + for (let i = 0; i < filterItems.length; i++) { + const item = filterItems[i]; + if (item.enabled && item.text.trim() !== '') { + 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 + setTimeout(() => { + const filterIndex = filterItems.findIndex((f) => f.text === item.text && f.enabled); + if (filterIndex !== -1) { + get().incrementFilterCount(filterIndex, comment.cid, comment.subplebbitAddress); + } + }, 0); + } + + if (item.hide) { + shouldHide = true; + } + } + } + } + + return !shouldHide; }, })); }, initializeFilter: () => { - const { updateFilter } = get(); - updateFilter(); + get().updateFilter(); + }, + incrementFilterCount: (filterIndex: number, cid: string, subplebbitAddress: string) => { + set((state) => { + const newFilterItems = [...state.filterItems]; + if (newFilterItems[filterIndex]) { + const item = newFilterItems[filterIndex]; + + // Ensure subplebbitFilteredCids is a Map + const subplebbitFilteredCids = new Map(item.subplebbitFilteredCids); + if (!subplebbitFilteredCids.has(subplebbitAddress)) { + subplebbitFilteredCids.set(subplebbitAddress, new Set()); + } + const cidSet = subplebbitFilteredCids.get(subplebbitAddress)!; + + // Only increment the count if this CID hasn't been counted for this subplebbit yet + if (!cidSet.has(cid)) { + const newItemFilteredCids = new Set(item.filteredCids); + newItemFilteredCids.add(cid); + + cidSet.add(cid); + subplebbitFilteredCids.set(subplebbitAddress, cidSet); + + const subplebbitCounts = new Map(item.subplebbitCounts); + const currentCount = subplebbitCounts.get(subplebbitAddress) || 0; + subplebbitCounts.set(subplebbitAddress, currentCount + 1); + + newFilterItems[filterIndex] = { + ...item, + count: item.count + 1, + filteredCids: newItemFilteredCids, + subplebbitCounts, + subplebbitFilteredCids, + }; + + return { filterItems: newFilterItems }; + } + } + return state; + }); + + get().recalcFilteredCount(); + }, + recalcFilteredCount: () => { + set((state) => { + const currentSubplebbit = state.currentSubplebbitAddress; + if (!currentSubplebbit) return { filteredCount: 0 }; + + let filteredCount = 0; + for (const item of state.filterItems) { + if (item.enabled && item.hide) { + const subCount = item.subplebbitCounts?.get(currentSubplebbit) || 0; + filteredCount += subCount; + } + } + + return { filteredCount }; + }); + }, + getFilteredCountForCurrentSubplebbit: () => { + const state = get(); + const currentSubplebbit = state.currentSubplebbitAddress; + if (!currentSubplebbit) return 0; + + let filteredCount = 0; + for (const item of state.filterItems) { + if (item.enabled && item.hide) { + const subCount = item.subplebbitCounts?.get(currentSubplebbit) || 0; + filteredCount += subCount; + } + } + + 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()); + + return newItem; + }); + + return { + filterItems: updatedFilterItems, + filteredCount: 0, + }; + }); + + // Trigger filter reapplication to start counting again + get().updateFilter(); }, - filteredCount: 0, - resetFilteredCount: () => set({ filteredCount: 0 }), }), { name: 'catalog-filters-storage', - onRehydrateStorage: () => (state) => { - if (state) { - state.updateFilter(); + partialize: (state) => { + return { + showTextOnlyThreads: state.showTextOnlyThreads, + filterItems: state.filterItems.map((item) => ({ + text: item.text, + enabled: item.enabled, + hide: item.hide, + top: item.top, + })), + } as any; + }, + deserialize: (persisted) => { + const persistedObj = typeof persisted === 'string' ? JSON.parse(persisted) : persisted; + + if (persistedObj && persistedObj.filterItems) { + return { + ...persistedObj, + filterItems: persistedObj.filterItems.map((item: any) => ({ + text: item.text, + enabled: item.enabled, + hide: item.hide, + top: item.top, + count: 0, + filteredCids: new Set(), + subplebbitCounts: new Map(), + subplebbitFilteredCids: new Map>(), + })), + filteredCount: 0, + }; } + return persistedObj || persisted; }, }, ), diff --git a/src/themes.css b/src/themes.css index f25059a3..93e6b01c 100644 --- a/src/themes.css +++ b/src/themes.css @@ -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; diff --git a/src/views/board/board.tsx b/src/views/board/board.tsx index 9f7472f6..fbdc2252 100644 --- a/src/views/board/board.tsx +++ b/src/views/board/board.tsx @@ -20,13 +20,16 @@ import { Post } from '../post'; const lastVirtuosoStates: { [key: string]: StateSnapshot } = {}; -const threadsWithoutImagesFilter = (comment: Comment) => { - const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {}; - if (!getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link)) { - return false; - } - return true; -}; +const createThreadsWithoutImagesFilter = () => ({ + filter: (comment: Comment) => { + const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {}; + if (!getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link)) { + return false; + } + return true; + }, + key: 'threads-with-images-only', +}); const Board = () => { const { t } = useTranslation(); @@ -66,7 +69,7 @@ const Board = () => { sortType, postsPerPage: isInAllView || isInSubscriptionsView || isInModView ? 5 : 25, ...(isInAllView || isInSubscriptionsView || isInModView ? { newerThan: timeFilterSeconds } : {}), - filter: hideThreadsWithoutImages ? threadsWithoutImagesFilter : undefined, + filter: hideThreadsWithoutImages ? createThreadsWithoutImagesFilter() : undefined, }; const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions); @@ -134,13 +137,13 @@ const Board = () => { subplebbitAddresses, sortType, newerThan: 60 * 60 * 24 * 7, - filter: hideThreadsWithoutImages ? threadsWithoutImagesFilter : undefined, + filter: hideThreadsWithoutImages ? createThreadsWithoutImagesFilter() : undefined, }); const { feed: monthlyFeed } = useFeed({ subplebbitAddresses, sortType, newerThan: 60 * 60 * 24 * 30, - filter: hideThreadsWithoutImages ? threadsWithoutImagesFilter : undefined, + filter: hideThreadsWithoutImages ? createThreadsWithoutImagesFilter() : undefined, }); const feedLength = feed.length; diff --git a/src/views/catalog/catalog-filters/catalog-filters.tsx b/src/views/catalog/catalog-filters/catalog-filters.tsx deleted file mode 100644 index e104f51e..00000000 --- a/src/views/catalog/catalog-filters/catalog-filters.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { useState, useCallback, useRef } from 'react'; -import { useTranslation } from 'react-i18next'; -import useCatalogFiltersStore from '../../../stores/use-catalog-filters-store'; -import styles from './catalog-filters.module.css'; - -const FiltersTable = ({ onSave }: { onSave: () => void }) => { - const { t } = useTranslation(); - const { filterItems, saveAndApplyFilters } = useCatalogFiltersStore(); - - const [localFilterItems, setLocalFilterItems] = useState(filterItems); - const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - - const handleAddFilter = useCallback(() => { - setLocalFilterItems((prev) => { - const newIndex = prev.length; - setTimeout(() => { - inputRefs.current[newIndex]?.focus(); - }, 0); - return [...prev, { text: '', enabled: true }]; - }); - }, []); - - const handleSave = useCallback(() => { - const nonEmptyFilters = localFilterItems.filter((item) => item.text.trim() !== ''); - saveAndApplyFilters(nonEmptyFilters); - onSave(); - }, [saveAndApplyFilters, localFilterItems, onSave]); - - const updateLocalFilterItem = useCallback((index: number, item: any) => { - setLocalFilterItems((prev) => prev.map((f, i) => (i === index ? item : f))); - }, []); - - const removeLocalFilterItem = useCallback((index: number) => { - setLocalFilterItems((prev) => prev.filter((_, i) => i !== index)); - }, []); - - const moveLocalFilterItemUp = useCallback((index: number) => { - if (index === 0) return; - setLocalFilterItems((prev) => { - const newItems = [...prev]; - [newItems[index - 1], newItems[index]] = [newItems[index], newItems[index - 1]]; - return newItems; - }); - }, []); - - return ( - - - - - - - - - - - {localFilterItems.map((item, index) => ( - - - - - - - ))} - - - - - - -
{t('order')}{t('enable')}{t('text')}{t('delete')}
- moveLocalFilterItemUp(index)}> - ↑ - - - updateLocalFilterItem(index, { ...item, enabled: e.target.checked })} - /> - - updateLocalFilterItem(index, { ...item, text: e.target.value })} - ref={(el) => (inputRefs.current[index] = el)} - /> - - removeLocalFilterItem(index)}> - × - -
- - -
- ); -}; - -const FiltersModal = ({ closeModal }: { closeModal: () => void }) => { - const { t } = useTranslation(); - - return ( - <> -
-
-
- {t('filters')} - -
- -
- - ); -}; - -const CatalogFilters = () => { - const { t } = useTranslation(); - const [showModal, setShowModal] = useState(false); - - const closeModal = () => { - setShowModal(false); - }; - - return ( - <> - setShowModal(false)} style={{ cursor: 'not-allowed' }}> - {t('filters')} - - {showModal && } - - ); -}; - -export default CatalogFilters; diff --git a/src/views/catalog/catalog.tsx b/src/views/catalog/catalog.tsx index 0beb6d24..b86487d8 100644 --- a/src/views/catalog/catalog.tsx +++ b/src/views/catalog/catalog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; import { Link, useLocation, useParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; import { Comment, useAccount, useFeed, useSubplebbit, useBlock, useAccountComments } from '@plebbit/plebbit-react-hooks'; @@ -14,18 +14,112 @@ import useCatalogStyleStore from '../../stores/use-catalog-style-store'; import useFeedResetStore from '../../stores/use-feed-reset-store'; import useInterfaceSettingsStore from '../../stores/use-interface-settings-store'; import useSortingStore from '../../stores/use-sorting-store'; +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 } = {}; -const threadsWithoutImagesFilter = (comment: Comment) => { - const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {}; - if (!getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link)) { - return false; - } - return true; +const createContentFilter = ( + filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set; hide: boolean; top: boolean; color?: string }[], + subplebbitAddress: string, + onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void, +) => { + // Create a unique key based on the enabled filter items + const enabledFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== ''); + const filterKey = + enabledFilters.length > 0 + ? `content-filter-${enabledFilters.map((item) => `${item.text}-${item.hide ? 'hide' : ''}-${item.top ? 'top' : ''}`).join('-')}` + : 'no-content-filter'; + + return { + filter: (comment: Comment) => { + if (!comment?.cid) return true; + + if (enabledFilters.length === 0) return true; + + // Check if any enabled filter matches the content + for (let i = 0; i < enabledFilters.length; i++) { + const item = enabledFilters[i]; + const pattern = item.text; + + 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) { + if (onFilterMatch) { + onFilterMatch(filterIndex, comment.cid, subplebbitAddress); + } else { + // Fallback to the store method if no callback provided + useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, comment.cid, subplebbitAddress); + } + + // If the filter has a color, track it in the matchedFilters map + if (item.color) { + useCatalogFiltersStore.getState().setMatchedFilter(comment.cid, item.color); + } + } + + // If this filter is set to hide, filter out the comment + if (item.hide) { + return false; + } + + // If this filter is set to top, we'll handle it separately in the component + // (we don't filter it out here) + } + } + + return true; + }, + key: filterKey, + }; +}; + +const createImageFilter = (showTextOnlyThreads: boolean) => { + return { + filter: (comment: Comment) => { + if (showTextOnlyThreads) return true; + + const { link, linkHeight, linkWidth, thumbnailUrl } = comment || {}; + const hasThumbnail = getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link); + + return hasThumbnail; + }, + key: showTextOnlyThreads ? 'no-image-filter' : 'threads-with-images-only', + }; +}; + +const createCombinedFilter = ( + showTextOnlyThreads: boolean, + filterItems: { text: string; enabled: boolean; count: number; filteredCids: Set; hide: boolean; top: boolean; color?: string }[], + searchText: string, + subplebbitAddress: string, + onFilterMatch?: (filterIndex: number, cid: string, subplebbitAddress: string) => void, +) => { + const imageFilter = createImageFilter(showTextOnlyThreads); + const contentFilter = createContentFilter(filterItems, subplebbitAddress, onFilterMatch); + + const searchFilter = { + filter: (comment: Comment) => { + if (!searchText.trim()) return true; + return commentMatchesPattern(comment, searchText); + }, + key: searchText ? `search-filter-${searchText}` : 'no-search-filter', + }; + + return { + filter: (comment: Comment) => { + if (!imageFilter.filter(comment)) return false; + if (!contentFilter.filter(comment)) return false; + if (!searchFilter.filter(comment)) return false; + + return true; + }, + key: `${imageFilter.key}-${contentFilter.key}-${searchFilter.key}`, + }; }; const Catalog = () => { @@ -36,7 +130,7 @@ const Catalog = () => { const isInAllView = isAllView(location.pathname); const defaultSubplebbits = useDefaultSubplebbits(); const { hideAdultBoards, hideGoreBoards } = useInterfaceSettingsStore(); - const { hideThreadsWithoutImages } = useInterfaceSettingsStore(); + const { showTextOnlyThreads, filterItems, searchText, clearMatchedFilters } = useCatalogFiltersStore(); const account = useAccount(); const subscriptions = account?.subscriptions; @@ -74,12 +168,25 @@ const Catalog = () => { const { timeFilterSeconds, timeFilterName } = useTimeFilter(); const { sortType } = useSortingStore(); + // Create a stable callback for filter matching + const handleFilterMatch = useCallback((filterIndex: number, cid: string, subplebbitAddress: string) => { + useCatalogFiltersStore.getState().incrementFilterCount(filterIndex, cid, subplebbitAddress); + }, []); + + // Set the current subplebbit address + useEffect(() => { + useCatalogFiltersStore.getState().setCurrentSubplebbitAddress(subplebbitAddress || null); + return () => { + useCatalogFiltersStore.getState().setCurrentSubplebbitAddress(null); + }; + }, [subplebbitAddress]); + const feedOptions = useMemo(() => { const options: any = { subplebbitAddresses, sortType, postsPerPage: isInAllView || isInSubscriptionsView ? 10 : postsPerPage, - filter: hideThreadsWithoutImages ? threadsWithoutImagesFilter : undefined, + filter: createCombinedFilter(showTextOnlyThreads, filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch), }; if (isInAllView || isInSubscriptionsView) { @@ -87,7 +194,19 @@ const Catalog = () => { } return options; - }, [subplebbitAddresses, sortType, isInAllView, isInSubscriptionsView, postsPerPage, timeFilterSeconds, hideThreadsWithoutImages]); + }, [ + subplebbitAddresses, + sortType, + isInAllView, + isInSubscriptionsView, + postsPerPage, + timeFilterSeconds, + showTextOnlyThreads, + filterItems, + searchText, + subplebbitAddress, + handleFilterMatch, + ]); const { feed, hasMore, loadMore, reset, subplebbitAddressesWithNewerPosts } = useFeed(feedOptions); const { accountComments } = useAccountComments(); @@ -99,19 +218,31 @@ const Catalog = () => { () => accountComments.filter((comment) => { const { cid, deleted, link, linkHeight, linkWidth, postCid, removed, state, thumbnailUrl, timestamp } = comment || {}; - return ( + + // Basic filtering conditions + const basicConditions = !deleted && !removed && timestamp > Date.now() / 1000 - 60 * 60 && state === 'succeeded' && cid && - (hideThreadsWithoutImages ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) && + (showTextOnlyThreads ? getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), comment?.link) : true) && cid === postCid && comment?.subplebbitAddress === subplebbitAddress && - !feed.some((post) => post.cid === cid) - ); + !feed.some((post) => post.cid === cid); + + // If search is active, also check search conditions + if (basicConditions && searchText.trim()) { + const titleLower = comment?.title?.toLowerCase() || ''; + const contentLower = comment?.content?.toLowerCase() || ''; + const searchPattern = searchText.toLowerCase(); + + return titleLower.includes(searchPattern) || contentLower.includes(searchPattern); + } + + return basicConditions; }), - [accountComments, subplebbitAddress, feed, hideThreadsWithoutImages], + [accountComments, subplebbitAddress, feed, showTextOnlyThreads, searchText], ); // show newest account comment at the top of the feed but after pinned posts @@ -143,13 +274,14 @@ const Catalog = () => { subplebbitAddresses, sortType, newerThan: 60 * 60 * 24 * 7, - filter: hideThreadsWithoutImages ? threadsWithoutImagesFilter : undefined, + filter: createCombinedFilter(showTextOnlyThreads, filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch), }); + const { feed: monthlyFeed } = useFeed({ subplebbitAddresses, sortType, newerThan: 60 * 60 * 24 * 30, - filter: hideThreadsWithoutImages ? threadsWithoutImagesFilter : undefined, + filter: createCombinedFilter(showTextOnlyThreads, filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch), }); const [showMorePostsSuggestion, setShowMorePostsSuggestion] = useState(false); @@ -262,7 +394,40 @@ const Catalog = () => { const isFeedLoaded = feed.length > 0 || state === 'failed'; - const rows = useCatalogFeedRows(columnCount, combinedFeed, isFeedLoaded, subplebbit); + // Process the feed to move "top" posts to the top + const processedFeed = useMemo(() => { + if (!combinedFeed || combinedFeed.length === 0) return combinedFeed; + + const enabledTopFilters = filterItems.filter((item) => item.enabled && item.text.trim() !== '' && item.top); + if (enabledTopFilters.length === 0) return combinedFeed; + + // Separate posts that match "top" filters + const topPosts: Comment[] = []; + const regularPosts: Comment[] = []; + + combinedFeed.forEach((comment) => { + if (!comment) return; + + let isTop = false; + for (const filter of enabledTopFilters) { + if (commentMatchesPattern(comment, filter.text)) { + isTop = true; + break; + } + } + + if (isTop) { + topPosts.push(comment); + } else { + regularPosts.push(comment); + } + }); + + // Return top posts followed by regular posts + return [...topPosts, ...regularPosts]; + }, [combinedFeed, filterItems]); + + const rows = useCatalogFeedRows(columnCount, processedFeed, isFeedLoaded, subplebbit); // save the last Virtuoso state to restore it when navigating back const virtuosoRef = useRef(null); @@ -286,11 +451,42 @@ const Catalog = () => { document.title = documentTitle + ` - ${t('catalog')} - plebchan`; }, [title, shortAddress, isInAllView, isInSubscriptionsView, t]); + // Clear matched filters when component mounts or when subplebbit changes + useEffect(() => { + clearMatchedFilters(); + return () => { + clearMatchedFilters(); + }; + }, [clearMatchedFilters, subplebbitAddress]); + + // Apply filter colors to posts when feed changes + useEffect(() => { + if (combinedFeed.length > 0 && filterItems.length > 0) { + // Clear existing matched filters + clearMatchedFilters(); + + // Apply colors to posts that match filters + combinedFeed.forEach((comment) => { + if (!comment?.cid) return; + + // Check each filter + for (const item of filterItems) { + if (item.enabled && item.text.trim() !== '' && item.color) { + if (commentMatchesPattern(comment, item.text)) { + useCatalogFiltersStore.getState().setMatchedFilter(comment.cid, item.color); + break; // Use the first matching filter's color + } + } + } + }); + } + }, [combinedFeed, filterItems, clearMatchedFilters]); + return (

- {combinedFeed.length !== 0 ? ( + {processedFeed?.length !== 0 ? ( <> { +export const Post = ({ post, showAllReplies = false, showReplies = true }: PostProps) => { const subplebbit = useSubplebbitsStore((state) => state.subplebbits[post?.subplebbitAddress]); const isMobile = useIsMobile(); @@ -39,9 +39,9 @@ export const Post = ({ post, showAllReplies = false, showReplies = true, replies
{isMobile ? ( - + ) : ( - + )}
@@ -57,10 +57,9 @@ const PostPage = () => { const isInDescriptionView = isDescriptionView(location.pathname, params); const isInRulesView = isRulesView(location.pathname, params); - const subplebbit = useSubplebbit({ subplebbitAddress }); - const { createdAt, description, rules, shortAddress, suggested, title } = subplebbit; - const comment = useComment({ commentCid }); + const subplebbit = useSubplebbit({ subplebbitAddress }); + const { createdAt, description, rules, shortAddress, suggested, title } = subplebbit || {}; // if the comment is a reply, return the post comment instead, then the reply will be highlighted in the thread const postComment = useComment({ commentCid: comment?.postCid }); diff --git a/yarn.lock b/yarn.lock index 42d0b932..b4233e39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3069,9 +3069,9 @@ dependencies: debug "4.3.4" -"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#6d35eb3b4dc84f8fdcbb46d95e3b1d78f1750b5f": +"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#54cf36c50dd31f5641a9475f5566b7b29b3873c7": version "0.0.1" - resolved "https://github.com/plebbit/plebbit-react-hooks.git#6d35eb3b4dc84f8fdcbb46d95e3b1d78f1750b5f" + resolved "https://github.com/plebbit/plebbit-react-hooks.git#54cf36c50dd31f5641a9475f5566b7b29b3873c7" dependencies: "@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#5ff48e9b9a906a098a51b474a52692eebfb6561d" "@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git" @@ -3976,6 +3976,21 @@ dependencies: "@types/node" "*" +"@typescript-eslint/eslint-plugin@8.26.0": + version "8.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz#7e880faf91f89471c30c141951e15f0eb3a0599e" + integrity sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "8.26.0" + "@typescript-eslint/type-utils" "8.26.0" + "@typescript-eslint/utils" "8.26.0" + "@typescript-eslint/visitor-keys" "8.26.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^2.0.1" + "@typescript-eslint/eslint-plugin@^5.5.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" @@ -3992,21 +4007,6 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/eslint-plugin@^8.26.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz#7e880faf91f89471c30c141951e15f0eb3a0599e" - integrity sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.26.0" - "@typescript-eslint/type-utils" "8.26.0" - "@typescript-eslint/utils" "8.26.0" - "@typescript-eslint/visitor-keys" "8.26.0" - graphemer "^1.4.0" - ignore "^5.3.1" - natural-compare "^1.4.0" - ts-api-utils "^2.0.1" - "@typescript-eslint/experimental-utils@^5.0.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz#14559bf73383a308026b427a4a6129bae2146741" @@ -4014,6 +4014,17 @@ dependencies: "@typescript-eslint/utils" "5.62.0" +"@typescript-eslint/parser@8.26.0": + version "8.26.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.0.tgz#9b4d2198e89f64fb81e83167eedd89a827d843a9" + integrity sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA== + dependencies: + "@typescript-eslint/scope-manager" "8.26.0" + "@typescript-eslint/types" "8.26.0" + "@typescript-eslint/typescript-estree" "8.26.0" + "@typescript-eslint/visitor-keys" "8.26.0" + debug "^4.3.4" + "@typescript-eslint/parser@^5.5.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" @@ -4024,17 +4035,6 @@ "@typescript-eslint/typescript-estree" "5.62.0" debug "^4.3.4" -"@typescript-eslint/parser@^8.26.0": - version "8.26.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.26.0.tgz#9b4d2198e89f64fb81e83167eedd89a827d843a9" - integrity sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA== - dependencies: - "@typescript-eslint/scope-manager" "8.26.0" - "@typescript-eslint/types" "8.26.0" - "@typescript-eslint/typescript-estree" "8.26.0" - "@typescript-eslint/visitor-keys" "8.26.0" - debug "^4.3.4" - "@typescript-eslint/scope-manager@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c"