feat: implement mod queue

This commit is contained in:
plebeius
2026-01-07 16:03:02 +01:00
parent 5a072a0307
commit 70ba14591a
46 changed files with 1143 additions and 41 deletions
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "حساب Bitsocial", "bitsocial_account": "حساب Bitsocial",
"p2p_options": "خيارات P2P", "p2p_options": "خيارات P2P",
"purge": "حذف نهائي", "purge": "حذف نهائي",
"purge_confirm": "سيؤدي الحذف النهائي إلى إزالة هذا المحتوى وجميع الردود عليه من قاعدة البيانات بشكل دائم. متابعة؟" "purge_confirm": "سيؤدي الحذف النهائي إلى إزالة هذا المحتوى وجميع الردود عليه من قاعدة البيانات بشكل دائم. متابعة؟",
"mod_queue": "قائمة انتظار الإشراف",
"moderation_queue": "قائمة انتظار الإشراف",
"queue_is_empty": "قائمة الانتظار فارغة",
"approve": "الموافقة",
"reject": "الرفض",
"waiting": "في الانتظار",
"filter_by_board": "تصفية حسب اللوحة",
"mod_queue_alert_threshold": "عتبة تنبيه قائمة انتظار الإشراف",
"mod_queue_alert_threshold_tip": "إظهار مؤشر أحمر وامض عندما تكون العناصر في الانتظار لأكثر من هذا الوقت",
"no_content": "لا يوجد محتوى",
"view": "عرض",
"excerpt": "مقتطف"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial অ্যাকাউন্ট", "bitsocial_account": "Bitsocial অ্যাকাউন্ট",
"p2p_options": "P2P বিকল্প", "p2p_options": "P2P বিকল্প",
"purge": "সর্বনাশ", "purge": "সর্বনাশ",
"purge_confirm": "পার্জ করা এই সামগ্রী এবং এর সমস্ত উত্তরকে ডাটাবেস থেকে স্থায়ীভাবে সরিয়ে দেবে। চালিয়ে যান?" "purge_confirm": "পার্জ করা এই সামগ্রী এবং এর সমস্ত উত্তরকে ডাটাবেস থেকে স্থায়ীভাবে সরিয়ে দেবে। চালিয়ে যান?",
"mod_queue": "মডারেশন সারি",
"moderation_queue": "মডারেশন সারি",
"queue_is_empty": "সারি খালি",
"approve": "অনুমোদন করুন",
"reject": "প্রত্যাখ্যান করুন",
"waiting": "অপেক্ষা করছে",
"filter_by_board": "বোর্ড অনুযায়ী ফিল্টার করুন",
"mod_queue_alert_threshold": "মডারেশন সারি সতর্কতা থ্রেশহোল্ড",
"mod_queue_alert_threshold_tip": "আইটেমগুলি এই সময়ের চেয়ে বেশি অপেক্ষা করলে একটি ঝলমলে লাল সূচক দেখান",
"no_content": "কোনো বিষয়বস্তু নেই",
"view": "দেখুন",
"excerpt": "উদ্ধৃতি"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Účet Bitsocial", "bitsocial_account": "Účet Bitsocial",
"p2p_options": "Možnosti P2P", "p2p_options": "Možnosti P2P",
"purge": "vymazat", "purge": "vymazat",
"purge_confirm": "Vymazání trvale odstraní tento obsah a všechny jeho odpovědi z databáze. Pokračovat?" "purge_confirm": "Vymazání trvale odstraní tento obsah a všechny jeho odpovědi z databáze. Pokračovat?",
"mod_queue": "Fronta moderování",
"moderation_queue": "Fronta moderování",
"queue_is_empty": "Fronta je prázdná",
"approve": "Schválit",
"reject": "Odmítnout",
"waiting": "Čeká",
"filter_by_board": "Filtrovat podle desky",
"mod_queue_alert_threshold": "Práh upozornění fronty moderování",
"mod_queue_alert_threshold_tip": "Zobrazit blikající červený indikátor, když položky čekají déle než tento čas",
"no_content": "Žádný obsah",
"view": "Zobrazit",
"excerpt": "Výňatek"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial-konto", "bitsocial_account": "Bitsocial-konto",
"p2p_options": "P2P-indstillinger", "p2p_options": "P2P-indstillinger",
"purge": "slet", "purge": "slet",
"purge_confirm": "Sletning fjerner permanent dette indhold og alle dets svar fra databasen. Fortsætte?" "purge_confirm": "Sletning fjerner permanent dette indhold og alle dets svar fra databasen. Fortsætte?",
"mod_queue": "Moderationskø",
"moderation_queue": "Moderationskø",
"queue_is_empty": "Køen er tom",
"approve": "Godkend",
"reject": "Afvis",
"waiting": "Venter",
"filter_by_board": "Filtrer efter tavle",
"mod_queue_alert_threshold": "Tærskel for moderationskø-alarm",
"mod_queue_alert_threshold_tip": "Vis en blinkende rød indikator, når elementer venter længere end denne tid",
"no_content": "Intet indhold",
"view": "Vis",
"excerpt": "Uddrag"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial-Konto", "bitsocial_account": "Bitsocial-Konto",
"p2p_options": "P2P-Optionen", "p2p_options": "P2P-Optionen",
"purge": "löschen", "purge": "löschen",
"purge_confirm": "Das Löschen entfernt diesen Inhalt und alle Antworten dauerhaft aus der Datenbank. Fortfahren?" "purge_confirm": "Das Löschen entfernt diesen Inhalt und alle Antworten dauerhaft aus der Datenbank. Fortfahren?",
"mod_queue": "Moderations-Warteschlange",
"moderation_queue": "Moderations-Warteschlange",
"queue_is_empty": "Warteschlange ist leer",
"approve": "Genehmigen",
"reject": "Ablehnen",
"waiting": "Wartend",
"filter_by_board": "Nach Forum filtern",
"mod_queue_alert_threshold": "Schwellenwert für Moderations-Warteschlangen-Alarm",
"mod_queue_alert_threshold_tip": "Einen blinkenden roten Indikator anzeigen, wenn Elemente länger als diese Zeit warten",
"no_content": "Kein Inhalt",
"view": "Anzeigen",
"excerpt": "Auszug"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Λογαριασμός Bitsocial", "bitsocial_account": "Λογαριασμός Bitsocial",
"p2p_options": "Επιλογές P2P", "p2p_options": "Επιλογές P2P",
"purge": "εκκαθάριση", "purge": "εκκαθάριση",
"purge_confirm": "Η εκκαθάριση θα αφαιρέσει μόνιμα αυτό το περιεχόμενο και όλες τις απαντήσεις του από τη βάση δεδομένων. Συνέχεια;" "purge_confirm": "Η εκκαθάριση θα αφαιρέσει μόνιμα αυτό το περιεχόμενο και όλες τις απαντήσεις του από τη βάση δεδομένων. Συνέχεια;",
"mod_queue": "Ουρά Συντονισμού",
"moderation_queue": "Ουρά Συντονισμού",
"queue_is_empty": "Η ουρά είναι άδεια",
"approve": "Έγκριση",
"reject": "Απόρριψη",
"waiting": "Αναμονή",
"filter_by_board": "Φίλτρο ανά σανίδα",
"mod_queue_alert_threshold": "Όριο ειδοποίησης ουράς συντονισμού",
"mod_queue_alert_threshold_tip": "Εμφάνιση αναβοσβήνοντος κόκκινου δείκτη όταν τα στοιχεία περιμένουν περισσότερο από αυτόν τον χρόνο",
"no_content": "Χωρίς περιεχόμενο",
"view": "Προβολή",
"excerpt": "Απόσπασμα"
} }
+13 -1
View File
@@ -253,5 +253,17 @@
"bitsocial_account": "Bitsocial Account", "bitsocial_account": "Bitsocial Account",
"p2p_options": "P2P options", "p2p_options": "P2P options",
"purge": "purge", "purge": "purge",
"purge_confirm": "Purging permanently removes this content and all its replies from the database. Continue?" "purge_confirm": "Purging permanently removes this content and all its replies from the database. Continue?",
"mod_queue": "Mod Queue",
"moderation_queue": "Moderation Queue",
"queue_is_empty": "Queue is empty",
"approve": "Approve",
"reject": "Reject",
"waiting": "Waiting",
"filter_by_board": "Filter by board",
"mod_queue_alert_threshold": "Mod queue alert threshold",
"mod_queue_alert_threshold_tip": "Show a blinking red indicator when items are waiting for longer than this time",
"no_content": "No content",
"view": "View",
"excerpt": "Excerpt"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Cuenta Bitsocial", "bitsocial_account": "Cuenta Bitsocial",
"p2p_options": "Opciones P2P", "p2p_options": "Opciones P2P",
"purge": "purgar", "purge": "purgar",
"purge_confirm": "Purgar eliminará permanentemente este contenido y todas sus respuestas de la base de datos. ¿Continuar?" "purge_confirm": "Purgar eliminará permanentemente este contenido y todas sus respuestas de la base de datos. ¿Continuar?",
"mod_queue": "Cola de Moderación",
"moderation_queue": "Cola de Moderación",
"queue_is_empty": "La cola está vacía",
"approve": "Aprobar",
"reject": "Rechazar",
"waiting": "Esperando",
"filter_by_board": "Filtrar por foro",
"mod_queue_alert_threshold": "Umbral de alerta de cola de moderación",
"mod_queue_alert_threshold_tip": "Mostrar un indicador rojo parpadeante cuando los elementos están esperando más de este tiempo",
"no_content": "Sin contenido",
"view": "Ver",
"excerpt": "Extracto"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "حساب Bitsocial", "bitsocial_account": "حساب Bitsocial",
"p2p_options": "گزینه‌های P2P", "p2p_options": "گزینه‌های P2P",
"purge": "پاکسازی", "purge": "پاکسازی",
"purge_confirm": "پاکسازی این محتوا و تمام پاسخ‌های آن را به طور دائم از پایگاه داده حذف می‌کند. ادامه دهید؟" "purge_confirm": "پاکسازی این محتوا و تمام پاسخ‌های آن را به طور دائم از پایگاه داده حذف می‌کند. ادامه دهید؟",
"mod_queue": "صف انتظار نظارت",
"moderation_queue": "صف انتظار نظارت",
"queue_is_empty": "صف خالی است",
"approve": "تایید",
"reject": "رد",
"waiting": "در انتظار",
"filter_by_board": "فیلتر بر اساس تابلو",
"mod_queue_alert_threshold": "آستانه هشدار صف انتظار نظارت",
"mod_queue_alert_threshold_tip": "نمایش نشانگر قرمز چشمک‌زن زمانی که موارد بیش از این زمان در انتظار هستند",
"no_content": "بدون محتوا",
"view": "مشاهده",
"excerpt": "گزیده"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial-tili", "bitsocial_account": "Bitsocial-tili",
"p2p_options": "P2P-asetukset", "p2p_options": "P2P-asetukset",
"purge": "poistaa", "purge": "poistaa",
"purge_confirm": "Poistaminen poistaa pysyvästi tämän sisällön ja kaikki sen vastaukset tietokannasta. Jatketaanko?" "purge_confirm": "Poistaminen poistaa pysyvästi tämän sisällön ja kaikki sen vastaukset tietokannasta. Jatketaanko?",
"mod_queue": "Moderaatiojono",
"moderation_queue": "Moderaatiojono",
"queue_is_empty": "Jono on tyhjä",
"approve": "Hyväksy",
"reject": "Hylkää",
"waiting": "Odottaa",
"filter_by_board": "Suodata taulun mukaan",
"mod_queue_alert_threshold": "Moderaatiojonon hälytyskynnys",
"mod_queue_alert_threshold_tip": "Näytä vilkkuva punainen indikaattori, kun kohteet odottavat pidempään kuin tämä aika",
"no_content": "Ei sisältöä",
"view": "Näytä",
"excerpt": "Ote"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Account ng Bitsocial", "bitsocial_account": "Account ng Bitsocial",
"p2p_options": "Mga Opsyon ng P2P", "p2p_options": "Mga Opsyon ng P2P",
"purge": "purga", "purge": "purga",
"purge_confirm": "Ang pag-purge ay permanenteng mag-aalis ng nilalamang ito at lahat ng mga tugon nito mula sa database. Magpatuloy?" "purge_confirm": "Ang pag-purge ay permanenteng mag-aalis ng nilalamang ito at lahat ng mga tugon nito mula sa database. Magpatuloy?",
"mod_queue": "Moderasyon Queue",
"moderation_queue": "Moderasyon Queue",
"queue_is_empty": "Walang laman ang queue",
"approve": "Aprubahan",
"reject": "Tanggihan",
"waiting": "Naghihintay",
"filter_by_board": "I-filter ayon sa board",
"mod_queue_alert_threshold": "Moderasyon queue alert threshold",
"mod_queue_alert_threshold_tip": "Magpakita ng kumikislap na pulang indicator kapag ang mga item ay naghihintay nang mas mahaba kaysa sa oras na ito",
"no_content": "Walang nilalaman",
"view": "Tingnan",
"excerpt": "Sipi"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Compte Bitsocial", "bitsocial_account": "Compte Bitsocial",
"p2p_options": "Options P2P", "p2p_options": "Options P2P",
"purge": "purger", "purge": "purger",
"purge_confirm": "La purge supprime définitivement ce contenu et toutes ses réponses de la base de données. Continuer?" "purge_confirm": "La purge supprime définitivement ce contenu et toutes ses réponses de la base de données. Continuer?",
"mod_queue": "File de Modération",
"moderation_queue": "File de Modération",
"queue_is_empty": "La file est vide",
"approve": "Approuver",
"reject": "Rejeter",
"waiting": "En attente",
"filter_by_board": "Filtrer par forum",
"mod_queue_alert_threshold": "Seuil d'alerte de la file de modération",
"mod_queue_alert_threshold_tip": "Afficher un indicateur rouge clignotant lorsque les éléments attendent plus longtemps que ce temps",
"no_content": "Aucun contenu",
"view": "Voir",
"excerpt": "Extrait"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "חשבון Bitsocial", "bitsocial_account": "חשבון Bitsocial",
"p2p_options": "אפשרויות P2P", "p2p_options": "אפשרויות P2P",
"purge": "למחוק", "purge": "למחוק",
"purge_confirm": "מחיקה תסיר לצמיתות את התוכן הזה ואת כל התגובות שלו מהמסד הנתונים. להמשיך?" "purge_confirm": "מחיקה תסיר לצמיתות את התוכן הזה ואת כל התגובות שלו מהמסד הנתונים. להמשיך?",
"mod_queue": "תור ניהול",
"moderation_queue": "תור ניהול",
"queue_is_empty": "התור ריק",
"approve": "אשר",
"reject": "דחה",
"waiting": "ממתין",
"filter_by_board": "סנן לפי לוח",
"mod_queue_alert_threshold": "סף התרעה של תור ניהול",
"mod_queue_alert_threshold_tip": "הצג אינדיקטור אדום מהבהב כאשר פריטים ממתינים יותר מזמן זה",
"no_content": "אין תוכן",
"view": "הצג",
"excerpt": "תקציר"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial खाता", "bitsocial_account": "Bitsocial खाता",
"p2p_options": "P2P विकल्प", "p2p_options": "P2P विकल्प",
"purge": "स्थायी रूप से हटाएं", "purge": "स्थायी रूप से हटाएं",
"purge_confirm": "स्थायी रूप से हटाने से यह सामग्री और इसकी सभी प्रतिक्रियाएं डेटाबेस से स्थायी रूप से हट जाएंगी। जारी रखें?" "purge_confirm": "स्थायी रूप से हटाने से यह सामग्री और इसकी सभी प्रतिक्रियाएं डेटाबेस से स्थायी रूप से हट जाएंगी। जारी रखें?",
"mod_queue": "मॉडरेटर कतार",
"moderation_queue": "मॉडरेटर कतार",
"queue_is_empty": "कतार खाली है",
"approve": "अनुमोदन करें",
"reject": "अस्वीकार करें",
"waiting": "प्रतीक्षा कर रहा है",
"filter_by_board": "बोर्ड के अनुसार फ़िल्टर करें",
"mod_queue_alert_threshold": "मॉडरेटर कतार सतर्कता सीमा",
"mod_queue_alert_threshold_tip": "जब आइटम इस समय से अधिक समय तक प्रतीक्षा कर रहे हों तो एक चमकता हुआ लाल संकेतक दिखाएं",
"no_content": "कोई सामग्री नहीं",
"view": "देखें",
"excerpt": "अंश"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial fiók", "bitsocial_account": "Bitsocial fiók",
"p2p_options": "P2P beállítások", "p2p_options": "P2P beállítások",
"purge": "törlés", "purge": "törlés",
"purge_confirm": "A törlés véglegesen eltávolítja ezt a tartalmat és az összes válaszát az adatbázisból. Folytatás?" "purge_confirm": "A törlés véglegesen eltávolítja ezt a tartalmat és az összes válaszát az adatbázisból. Folytatás?",
"mod_queue": "Moderációs Sor",
"moderation_queue": "Moderációs Sor",
"queue_is_empty": "A sor üres",
"approve": "Jóváhagyás",
"reject": "Elutasítás",
"waiting": "Várakozás",
"filter_by_board": "Szűrés tábla szerint",
"mod_queue_alert_threshold": "Moderációs sor riasztási küszöb",
"mod_queue_alert_threshold_tip": "Villogó piros jelző megjelenítése, amikor az elemek ennél hosszabb ideig várakoznak",
"no_content": "Nincs tartalom",
"view": "Megtekintés",
"excerpt": "Kivonat"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Akun Bitsocial", "bitsocial_account": "Akun Bitsocial",
"p2p_options": "Opsi P2P", "p2p_options": "Opsi P2P",
"purge": "hapus", "purge": "hapus",
"purge_confirm": "Menghapus akan menghapus konten ini dan semua balasannya secara permanen dari database. Lanjutkan?" "purge_confirm": "Menghapus akan menghapus konten ini dan semua balasannya secara permanen dari database. Lanjutkan?",
"mod_queue": "Antrian Moderasi",
"moderation_queue": "Antrian Moderasi",
"queue_is_empty": "Antrian kosong",
"approve": "Setujui",
"reject": "Tolak",
"waiting": "Menunggu",
"filter_by_board": "Filter berdasarkan papan",
"mod_queue_alert_threshold": "Ambang peringatan antrian moderasi",
"mod_queue_alert_threshold_tip": "Tampilkan indikator merah berkedip ketika item menunggu lebih lama dari waktu ini",
"no_content": "Tidak ada konten",
"view": "Lihat",
"excerpt": "Kutipan"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Account Bitsocial", "bitsocial_account": "Account Bitsocial",
"p2p_options": "Opzioni P2P", "p2p_options": "Opzioni P2P",
"purge": "elimina", "purge": "elimina",
"purge_confirm": "L'eliminazione rimuoverà permanentemente questo contenuto e tutte le sue risposte dal database. Continuare?" "purge_confirm": "L'eliminazione rimuoverà permanentemente questo contenuto e tutte le sue risposte dal database. Continuare?",
"mod_queue": "Coda Moderazione",
"moderation_queue": "Coda di Moderazione",
"queue_is_empty": "La coda è vuota",
"approve": "Approva",
"reject": "Rifiuta",
"waiting": "In attesa",
"filter_by_board": "Filtra per board",
"mod_queue_alert_threshold": "Soglia di allerta coda moderazione",
"mod_queue_alert_threshold_tip": "Mostra un indicatore rosso lampeggiante quando gli elementi attendono più di questo tempo",
"no_content": "Nessun contenuto",
"view": "Visualizza",
"excerpt": "Estratto"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocialアカウント", "bitsocial_account": "Bitsocialアカウント",
"p2p_options": "P2Pオプション", "p2p_options": "P2Pオプション",
"purge": "削除", "purge": "削除",
"purge_confirm": "削除すると、このコンテンツとそのすべての返信がデータベースから永続的に削除されます。続行しますか?" "purge_confirm": "削除すると、このコンテンツとそのすべての返信がデータベースから永続的に削除されます。続行しますか?",
"mod_queue": "モデレーションキュー",
"moderation_queue": "モデレーションキュー",
"queue_is_empty": "キューは空です",
"approve": "承認",
"reject": "拒否",
"waiting": "待機中",
"filter_by_board": "ボードでフィルター",
"mod_queue_alert_threshold": "モデレーションキューのアラート閾値",
"mod_queue_alert_threshold_tip": "アイテムがこの時間より長く待機している場合、点滅する赤いインジケーターを表示",
"no_content": "コンテンツなし",
"view": "表示",
"excerpt": "抜粋"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial 계정", "bitsocial_account": "Bitsocial 계정",
"p2p_options": "P2P 옵션", "p2p_options": "P2P 옵션",
"purge": "삭제", "purge": "삭제",
"purge_confirm": "삭제하면 이 콘텐츠와 모든 답글이 데이터베이스에서 영구적으로 제거됩니다. 계속하시겠습니까?" "purge_confirm": "삭제하면 이 콘텐츠와 모든 답글이 데이터베이스에서 영구적으로 제거됩니다. 계속하시겠습니까?",
"mod_queue": "관리자 대기열",
"moderation_queue": "관리자 대기열",
"queue_is_empty": "대기열이 비어 있습니다",
"approve": "승인",
"reject": "거부",
"waiting": "대기 중",
"filter_by_board": "게시판별 필터",
"mod_queue_alert_threshold": "관리자 대기열 알림 임계값",
"mod_queue_alert_threshold_tip": "항목이 이 시간보다 오래 대기 중일 때 깜빡이는 빨간색 표시기 표시",
"no_content": "콘텐츠 없음",
"view": "보기",
"excerpt": "요약"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial खाते", "bitsocial_account": "Bitsocial खाते",
"p2p_options": "P2P पर्याय", "p2p_options": "P2P पर्याय",
"purge": "हटवा", "purge": "हटवा",
"purge_confirm": "पर्ज करणे या सामग्रीला आणि त्याच्या सर्व प्रतिक्रियांना डेटाबेसमधून कायमस्वरूपी काढून टाकेल. पुढे जाऊ?" "purge_confirm": "पर्ज करणे या सामग्रीला आणि त्याच्या सर्व प्रतिक्रियांना डेटाबेसमधून कायमस्वरूपी काढून टाकेल. पुढे जाऊ?",
"mod_queue": "मॉडरेशन रांग",
"moderation_queue": "मॉडरेशन रांग",
"queue_is_empty": "रांग रिकामी आहे",
"approve": "मंजूर करा",
"reject": "नकार द्या",
"waiting": "प्रतीक्षा करत आहे",
"filter_by_board": "बोर्डनुसार फिल्टर करा",
"mod_queue_alert_threshold": "मॉडरेशन रांग सतर्कता मर्यादा",
"mod_queue_alert_threshold_tip": "जेव्हा आयटम या वेळेपेक्षा जास्त वेळ प्रतीक्षा करत असतात तेव्हा एक चमकणारा लाल सूचक दाखवा",
"no_content": "सामग्री नाही",
"view": "पहा",
"excerpt": "उतारा"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial-account", "bitsocial_account": "Bitsocial-account",
"p2p_options": "P2P-opties", "p2p_options": "P2P-opties",
"purge": "verwijderen", "purge": "verwijderen",
"purge_confirm": "Verwijderen verwijdert deze inhoud en alle antwoorden ervan permanent uit de database. Doorgaan?" "purge_confirm": "Verwijderen verwijdert deze inhoud en alle antwoorden ervan permanent uit de database. Doorgaan?",
"mod_queue": "Moderatie Wachtrij",
"moderation_queue": "Moderatie Wachtrij",
"queue_is_empty": "Wachtrij is leeg",
"approve": "Goedkeuren",
"reject": "Afwijzen",
"waiting": "Wachten",
"filter_by_board": "Filteren op bord",
"mod_queue_alert_threshold": "Drempelwaarde voor moderatiewachtrijwaarschuwing",
"mod_queue_alert_threshold_tip": "Toon een knipperende rode indicator wanneer items langer dan deze tijd wachten",
"no_content": "Geen inhoud",
"view": "Bekijken",
"excerpt": "Uittreksel"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial-konto", "bitsocial_account": "Bitsocial-konto",
"p2p_options": "P2P-alternativer", "p2p_options": "P2P-alternativer",
"purge": "slette", "purge": "slette",
"purge_confirm": "Sletting fjerner permanent dette innholdet og alle svarene fra databasen. Fortsette?" "purge_confirm": "Sletting fjerner permanent dette innholdet og alle svarene fra databasen. Fortsette?",
"mod_queue": "Modereringskø",
"moderation_queue": "Modereringskø",
"queue_is_empty": "Køen er tom",
"approve": "Godkjenn",
"reject": "Avvis",
"waiting": "Venter",
"filter_by_board": "Filtrer etter tavle",
"mod_queue_alert_threshold": "Terskel for modereringskø-varsel",
"mod_queue_alert_threshold_tip": "Vis en blinkende rød indikator når elementer venter lenger enn denne tiden",
"no_content": "Ingen innhold",
"view": "Vis",
"excerpt": "Utdrag"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Konto Bitsocial", "bitsocial_account": "Konto Bitsocial",
"p2p_options": "Opcje P2P", "p2p_options": "Opcje P2P",
"purge": "usuń", "purge": "usuń",
"purge_confirm": "Usunięcie trwale usunie tę zawartość i wszystkie odpowiedzi z bazy danych. Kontynuować?" "purge_confirm": "Usunięcie trwale usunie tę zawartość i wszystkie odpowiedzi z bazy danych. Kontynuować?",
"mod_queue": "Kolejka Moderacji",
"moderation_queue": "Kolejka Moderacji",
"queue_is_empty": "Kolejka jest pusta",
"approve": "Zatwierdź",
"reject": "Odrzuć",
"waiting": "Oczekiwanie",
"filter_by_board": "Filtruj według tablicy",
"mod_queue_alert_threshold": "Próg alertu kolejki moderacji",
"mod_queue_alert_threshold_tip": "Pokaż migający czerwony wskaźnik, gdy elementy czekają dłużej niż ten czas",
"no_content": "Brak zawartości",
"view": "Zobacz",
"excerpt": "Fragment"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Conta Bitsocial", "bitsocial_account": "Conta Bitsocial",
"p2p_options": "Opções P2P", "p2p_options": "Opções P2P",
"purge": "purgar", "purge": "purgar",
"purge_confirm": "Purgar removerá permanentemente este conteúdo e todas as suas respostas do banco de dados. Continuar?" "purge_confirm": "Purgar removerá permanentemente este conteúdo e todas as suas respostas do banco de dados. Continuar?",
"mod_queue": "Fila de Moderação",
"moderation_queue": "Fila de Moderação",
"queue_is_empty": "A fila está vazia",
"approve": "Aprovar",
"reject": "Rejeitar",
"waiting": "Aguardando",
"filter_by_board": "Filtrar por quadro",
"mod_queue_alert_threshold": "Limite de alerta da fila de moderação",
"mod_queue_alert_threshold_tip": "Mostrar um indicador vermelho piscante quando os itens estão aguardando por mais tempo que este tempo",
"no_content": "Sem conteúdo",
"view": "Ver",
"excerpt": "Trecho"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Cont Bitsocial", "bitsocial_account": "Cont Bitsocial",
"p2p_options": "Opțiuni P2P", "p2p_options": "Opțiuni P2P",
"purge": "șterge", "purge": "șterge",
"purge_confirm": "Ștergerea va elimina permanent acest conținut și toate răspunsurile sale din baza de date. Continuați?" "purge_confirm": "Ștergerea va elimina permanent acest conținut și toate răspunsurile sale din baza de date. Continuați?",
"mod_queue": "Coadă de Moderare",
"moderation_queue": "Coadă de Moderare",
"queue_is_empty": "Coada este goală",
"approve": "Aprobă",
"reject": "Respinge",
"waiting": "Așteaptă",
"filter_by_board": "Filtrează după panou",
"mod_queue_alert_threshold": "Prag de alertă pentru coada de moderare",
"mod_queue_alert_threshold_tip": "Afișează un indicator roșu intermitent când elementele așteaptă mai mult decât acest timp",
"no_content": "Fără conținut",
"view": "Vizualizează",
"excerpt": "Fragment"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Аккаунт Bitsocial", "bitsocial_account": "Аккаунт Bitsocial",
"p2p_options": "Опции P2P", "p2p_options": "Опции P2P",
"purge": "удалить", "purge": "удалить",
"purge_confirm": "Удаление навсегда удалит этот контент и все ответы на него из базы данных. Продолжить?" "purge_confirm": "Удаление навсегда удалит этот контент и все ответы на него из базы данных. Продолжить?",
"mod_queue": "Очередь Модерации",
"moderation_queue": "Очередь Модерации",
"queue_is_empty": "Очередь пуста",
"approve": "Одобрить",
"reject": "Отклонить",
"waiting": "Ожидание",
"filter_by_board": "Фильтр по доске",
"mod_queue_alert_threshold": "Порог предупреждения очереди модерации",
"mod_queue_alert_threshold_tip": "Показывать мигающий красный индикатор, когда элементы ждут дольше этого времени",
"no_content": "Нет содержимого",
"view": "Просмотр",
"excerpt": "Выдержка"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Llogaria Bitsocial", "bitsocial_account": "Llogaria Bitsocial",
"p2p_options": "Opsionet P2P", "p2p_options": "Opsionet P2P",
"purge": "fshi", "purge": "fshi",
"purge_confirm": "Fshirja do të heqë përgjithmonë këtë përmbajtje dhe të gjitha përgjigjet e saj nga baza e të dhënave. Vazhdo?" "purge_confirm": "Fshirja do të heqë përgjithmonë këtë përmbajtje dhe të gjitha përgjigjet e saj nga baza e të dhënave. Vazhdo?",
"mod_queue": "Radha e Moderimit",
"moderation_queue": "Radha e Moderimit",
"queue_is_empty": "Radha është e zbrazët",
"approve": "Aprovo",
"reject": "Refuzo",
"waiting": "Duke pritur",
"filter_by_board": "Filtro sipas tabelës",
"mod_queue_alert_threshold": "Pragu i alarmit të radhës së moderimit",
"mod_queue_alert_threshold_tip": "Shfaq një tregues të kuq që blinkon kur elementët po presin më shumë se kjo kohë",
"no_content": "Pa përmbajtje",
"view": "Shiko",
"excerpt": "Fragment"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial-konto", "bitsocial_account": "Bitsocial-konto",
"p2p_options": "P2P-alternativ", "p2p_options": "P2P-alternativ",
"purge": "radera", "purge": "radera",
"purge_confirm": "Raderande tar permanent bort detta innehåll och alla dess svar från databasen. Fortsätta?" "purge_confirm": "Raderande tar permanent bort detta innehåll och alla dess svar från databasen. Fortsätta?",
"mod_queue": "Modereringskö",
"moderation_queue": "Modereringskö",
"queue_is_empty": "Kön är tom",
"approve": "Godkänn",
"reject": "Avvisa",
"waiting": "Väntar",
"filter_by_board": "Filtrera efter tavla",
"mod_queue_alert_threshold": "Tröskelvärde för modereringskö-varning",
"mod_queue_alert_threshold_tip": "Visa en blinkande röd indikator när objekt väntar längre än denna tid",
"no_content": "Inget innehåll",
"view": "Visa",
"excerpt": "Utdrag"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial ఖాతా", "bitsocial_account": "Bitsocial ఖాతా",
"p2p_options": "P2P ఎంపికలు", "p2p_options": "P2P ఎంపికలు",
"purge": "తొలగించు", "purge": "తొలగించు",
"purge_confirm": "పర్జ్ చేయడం ఈ కంటెంట్ మరియు దాని అన్ని ప్రతిస్పందనలను డేటాబేస్ నుండి శాశ్వతంగా తొలగిస్తుంది. కొనసాగించాలా?" "purge_confirm": "పర్జ్ చేయడం ఈ కంటెంట్ మరియు దాని అన్ని ప్రతిస్పందనలను డేటాబేస్ నుండి శాశ్వతంగా తొలగిస్తుంది. కొనసాగించాలా?",
"mod_queue": "మోడరేషన్ క్యూ",
"moderation_queue": "మోడరేషన్ క్యూ",
"queue_is_empty": "క్యూ ఖాళీగా ఉంది",
"approve": "ఆమోదించు",
"reject": "తిరస్కరించు",
"waiting": "వేచి ఉంది",
"filter_by_board": "బోర్డ్ ద్వారా ఫిల్టర్ చేయి",
"mod_queue_alert_threshold": "మోడరేషన్ క్యూ హెచ్చరిక థ్రెషోల్డ్",
"mod_queue_alert_threshold_tip": "అంశాలు ఈ సమయం కంటే ఎక్కువ సమయం వేచి ఉన్నప్పుడు మెరిసే ఎరుపు సూచికను చూపించు",
"no_content": "కంటెంట్ లేదు",
"view": "చూడు",
"excerpt": "ఎక్సెర్ప్ట్"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "บัญชี Bitsocial", "bitsocial_account": "บัญชี Bitsocial",
"p2p_options": "ตัวเลือก P2P", "p2p_options": "ตัวเลือก P2P",
"purge": "ลบ", "purge": "ลบ",
"purge_confirm": "การลบจะลบเนื้อหานี้และคำตอบทั้งหมดออกจากฐานข้อมูลอย่างถาวร ดำเนินการต่อหรือไม่?" "purge_confirm": "การลบจะลบเนื้อหานี้และคำตอบทั้งหมดออกจากฐานข้อมูลอย่างถาวร ดำเนินการต่อหรือไม่?",
"mod_queue": "คิวผู้ดูแล",
"moderation_queue": "คิวผู้ดูแล",
"queue_is_empty": "คิวว่างเปล่า",
"approve": "อนุมัติ",
"reject": "ปฏิเสธ",
"waiting": "กำลังรอ",
"filter_by_board": "กรองตามบอร์ด",
"mod_queue_alert_threshold": "เกณฑ์การแจ้งเตือนคิวผู้ดูแล",
"mod_queue_alert_threshold_tip": "แสดงตัวบ่งชี้สีแดงกะพริบเมื่อรายการรอเกินเวลานี้",
"no_content": "ไม่มีเนื้อหา",
"view": "ดู",
"excerpt": "ข้อความย่อ"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial Hesabı", "bitsocial_account": "Bitsocial Hesabı",
"p2p_options": "P2P Seçenekleri", "p2p_options": "P2P Seçenekleri",
"purge": "temizle", "purge": "temizle",
"purge_confirm": "Temizleme, bu içeriği ve tüm yanıtlarını veritabanından kalıcı olarak kaldıracaktır. Devam edilsin mi?" "purge_confirm": "Temizleme, bu içeriği ve tüm yanıtlarını veritabanından kalıcı olarak kaldıracaktır. Devam edilsin mi?",
"mod_queue": "Moderasyon Kuyruğu",
"moderation_queue": "Moderasyon Kuyruğu",
"queue_is_empty": "Kuyruk boş",
"approve": "Onayla",
"reject": "Reddet",
"waiting": "Bekliyor",
"filter_by_board": "Panoya göre filtrele",
"mod_queue_alert_threshold": "Moderasyon kuyruğu uyarı eşiği",
"mod_queue_alert_threshold_tip": "Öğeler bu süreden daha uzun süre beklediğinde yanıp sönen kırmızı bir gösterge göster",
"no_content": "İçerik yok",
"view": "Görüntüle",
"excerpt": "Alıntı"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Обліковий запис Bitsocial", "bitsocial_account": "Обліковий запис Bitsocial",
"p2p_options": "Опції P2P", "p2p_options": "Опції P2P",
"purge": "видалити", "purge": "видалити",
"purge_confirm": "Видалення назавжди видалить цей контент і всі відповіді на нього з бази даних. Продовжити?" "purge_confirm": "Видалення назавжди видалить цей контент і всі відповіді на нього з бази даних. Продовжити?",
"mod_queue": "Черга Модерації",
"moderation_queue": "Черга Модерації",
"queue_is_empty": "Черга порожня",
"approve": "Схвалити",
"reject": "Відхилити",
"waiting": "Очікування",
"filter_by_board": "Фільтр за дошкою",
"mod_queue_alert_threshold": "Поріг попередження черги модерації",
"mod_queue_alert_threshold_tip": "Показувати мигаючий червоний індикатор, коли елементи чекають довше цього часу",
"no_content": "Немає вмісту",
"view": "Перегляд",
"excerpt": "Витяг"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial اکاؤنٹ", "bitsocial_account": "Bitsocial اکاؤنٹ",
"p2p_options": "P2P اختیارات", "p2p_options": "P2P اختیارات",
"purge": "ختم کرنا", "purge": "ختم کرنا",
"purge_confirm": "پرج کرنا اس مواد اور اس کی تمام جوابات کو ڈیٹا بیس سے مستقل طور پر ہٹا دے گا۔ جاری رکھیں؟" "purge_confirm": "پرج کرنا اس مواد اور اس کی تمام جوابات کو ڈیٹا بیس سے مستقل طور پر ہٹا دے گا۔ جاری رکھیں؟",
"mod_queue": "نگرانی کی قطار",
"moderation_queue": "نگرانی کی قطار",
"queue_is_empty": "قطار خالی ہے",
"approve": "منظور کریں",
"reject": "مسترد کریں",
"waiting": "انتظار کر رہا ہے",
"filter_by_board": "بورڈ کے لحاظ سے فلٹر کریں",
"mod_queue_alert_threshold": "نگرانی کی قطار کی انتباہ کی حد",
"mod_queue_alert_threshold_tip": "جب آئٹمز اس وقت سے زیادہ انتظار کر رہے ہوں تو ایک ٹمٹمانے والا سرخ اشارہ دکھائیں",
"no_content": "کوئی مواد نہیں",
"view": "دیکھیں",
"excerpt": "اقتباس"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Tài khoản Bitsocial", "bitsocial_account": "Tài khoản Bitsocial",
"p2p_options": "Tùy chọn P2P", "p2p_options": "Tùy chọn P2P",
"purge": "xóa", "purge": "xóa",
"purge_confirm": "Xóa sẽ loại bỏ vĩnh viễn nội dung này và tất cả các phản hồi của nó khỏi cơ sở dữ liệu. Tiếp tục?" "purge_confirm": "Xóa sẽ loại bỏ vĩnh viễn nội dung này và tất cả các phản hồi của nó khỏi cơ sở dữ liệu. Tiếp tục?",
"mod_queue": "Hàng Đợi Kiểm Duyệt",
"moderation_queue": "Hàng Đợi Kiểm Duyệt",
"queue_is_empty": "Hàng đợi trống",
"approve": "Phê duyệt",
"reject": "Từ chối",
"waiting": "Đang chờ",
"filter_by_board": "Lọc theo bảng",
"mod_queue_alert_threshold": "Ngưỡng cảnh báo hàng đợi kiểm duyệt",
"mod_queue_alert_threshold_tip": "Hiển thị chỉ báo màu đỏ nhấp nháy khi các mục đang chờ lâu hơn thời gian này",
"no_content": "Không có nội dung",
"view": "Xem",
"excerpt": "Trích đoạn"
} }
+13 -1
View File
@@ -254,5 +254,17 @@
"bitsocial_account": "Bitsocial账户", "bitsocial_account": "Bitsocial账户",
"p2p_options": "P2P选项", "p2p_options": "P2P选项",
"purge": "清除", "purge": "清除",
"purge_confirm": "清除将永久删除此内容及其所有回复。继续吗?" "purge_confirm": "清除将永久删除此内容及其所有回复。继续吗?",
"mod_queue": "审核队列",
"moderation_queue": "审核队列",
"queue_is_empty": "队列为空",
"approve": "批准",
"reject": "拒绝",
"waiting": "等待中",
"filter_by_board": "按板块筛选",
"mod_queue_alert_threshold": "审核队列警报阈值",
"mod_queue_alert_threshold_tip": "当项目等待时间超过此时长时显示闪烁的红色指示器",
"no_content": "无内容",
"view": "查看",
"excerpt": "摘要"
} }
+10 -2
View File
@@ -10,7 +10,7 @@ import useSpecialThemeStore from './stores/use-special-theme-store';
import useIsMobile from './hooks/use-is-mobile'; import useIsMobile from './hooks/use-is-mobile';
import useTheme from './hooks/use-theme'; import useTheme from './hooks/use-theme';
import { useDefaultSubplebbits } from './hooks/use-default-subplebbits'; import { useDefaultSubplebbits } from './hooks/use-default-subplebbits';
import { getSubplebbitAddress, isPostRoute, isPendingPostRoute } from './lib/utils/route-utils'; import { getSubplebbitAddress, isPostRoute, isPendingPostRoute, isModQueueRoute } from './lib/utils/route-utils';
import styles from './app.module.css'; import styles from './app.module.css';
import FAQ from './views/faq'; import FAQ from './views/faq';
import Home from './views/home'; import Home from './views/home';
@@ -18,6 +18,7 @@ import Rules from './views/rules';
import NotFound from './views/not-found'; import NotFound from './views/not-found';
import PendingPost from './views/pending-post'; import PendingPost from './views/pending-post';
import Post from './views/post'; import Post from './views/post';
import ModQueueView from './views/mod-queue';
import { DesktopBoardButtons, MobileBoardButtons } from './components/board-buttons'; import { DesktopBoardButtons, MobileBoardButtons } from './components/board-buttons';
import BoardHeader from './components/board-header'; import BoardHeader from './components/board-header';
import ChallengeModal from './components/challenge-modal'; import ChallengeModal from './components/challenge-modal';
@@ -50,6 +51,7 @@ const BoardLayout = () => {
const isOnPostRoute = isPostRoute(location.pathname); const isOnPostRoute = isPostRoute(location.pathname);
const isOnPendingPostRoute = isPendingPostRoute(location.pathname); const isOnPendingPostRoute = isPendingPostRoute(location.pathname);
const isOnModQueueRoute = isModQueueRoute(location.pathname);
// Christmas theme // Christmas theme
const { isEnabled: isSpecialEnabled } = useSpecialThemeStore(); const { isEnabled: isSpecialEnabled } = useSpecialThemeStore();
@@ -95,7 +97,7 @@ const BoardLayout = () => {
</> </>
)} )}
<FeedCacheContainer /> <FeedCacheContainer />
{(isOnPostRoute || isOnPendingPostRoute) && <Outlet />} {(isOnPostRoute || isOnPendingPostRoute || isOnModQueueRoute) && <Outlet />}
</div> </div>
); );
}; };
@@ -168,11 +170,17 @@ const App = () => (
<Route path='/mod/catalog/:timeFilterName?' element={null} /> <Route path='/mod/catalog/:timeFilterName?' element={null} />
<Route path='/mod/catalog/:timeFilterName?/settings' element={null} /> <Route path='/mod/catalog/:timeFilterName?/settings' element={null} />
<Route path='/mod/queue' element={<ModQueueView />} />
<Route path='/mod/queue/settings' element={<ModQueueView />} />
<Route path='/:boardIdentifier' element={null} /> <Route path='/:boardIdentifier' element={null} />
<Route path='/:boardIdentifier/settings' element={null} /> <Route path='/:boardIdentifier/settings' element={null} />
<Route path='/:boardIdentifier/catalog' element={null} /> <Route path='/:boardIdentifier/catalog' element={null} />
<Route path='/:boardIdentifier/catalog/settings' element={null} /> <Route path='/:boardIdentifier/catalog/settings' element={null} />
<Route path='/:boardIdentifier/queue' element={<ModQueueView />} />
<Route path='/:boardIdentifier/queue/settings' element={<ModQueueView />} />
<Route path='/:boardIdentifier/thread/:commentCid' element={<Post />} /> <Route path='/:boardIdentifier/thread/:commentCid' element={<Post />} />
<Route path='/:boardIdentifier/thread/:commentCid/settings' element={<Post />} /> <Route path='/:boardIdentifier/thread/:commentCid/settings' element={<Post />} />
+35 -2
View File
@@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next';
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks'; import { useAccountComment, useSubscribe } from '@plebbit/plebbit-react-hooks';
import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages';
import { isAllView, isCatalogView, isModView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils'; import { isAllView, isCatalogView, isModView, isModQueueView, isPendingPostView, isPostPageView, isSubscriptionsView } from '../../lib/utils/view-utils';
import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits'; import { useDefaultSubplebbits } from '../../hooks/use-default-subplebbits';
import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils'; import { getBoardPath, isDirectoryBoard } from '../../lib/utils/route-utils';
import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address'; import { useResolvedSubplebbitAddress } from '../../hooks/use-resolved-subplebbit-address';
@@ -26,6 +26,7 @@ interface BoardButtonsProps {
isInCatalogView?: boolean; isInCatalogView?: boolean;
isInSubscriptionsView?: boolean; isInSubscriptionsView?: boolean;
isInModView?: boolean; isInModView?: boolean;
isInModQueueView?: boolean;
isTopbar?: boolean; isTopbar?: boolean;
} }
@@ -72,7 +73,7 @@ const SubscribeButton = ({ address }: BoardButtonsProps) => {
); );
}; };
const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView }: BoardButtonsProps) => { const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView, isInModQueueView }: BoardButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const params = useParams(); const params = useParams();
const defaultSubplebbits = useDefaultSubplebbits(); const defaultSubplebbits = useDefaultSubplebbits();
@@ -84,6 +85,12 @@ const ReturnButton = ({ address, isInAllView, isInSubscriptionsView, isInModView
} else if (isInSubscriptionsView) { } else if (isInSubscriptionsView) {
if (params?.timeFilterName) return `/subs/${params.timeFilterName}`; if (params?.timeFilterName) return `/subs/${params.timeFilterName}`;
return `/subs`; return `/subs`;
} else if (isInModQueueView) {
// If in mod queue view, return to /mod or /:boardIdentifier
if (params?.boardIdentifier) {
return `/${params.boardIdentifier}`;
}
return `/mod`;
} else if (isInModView) { } else if (isInModView) {
if (params?.timeFilterName) return `/mod/${params.timeFilterName}`; if (params?.timeFilterName) return `/mod/${params.timeFilterName}`;
return `/mod`; return `/mod`;
@@ -309,6 +316,7 @@ export const MobileBoardButtons = () => {
const isInPostView = isPostPageView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname); const isInModView = isModView(location.pathname);
const isInModQueueView = isModQueueView(location.pathname);
const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any }); const accountComment = useAccountComment({ commentIndex: params?.accountCommentIndex as any });
const resolvedAddress = useResolvedSubplebbitAddress(); const resolvedAddress = useResolvedSubplebbitAddress();
@@ -334,6 +342,17 @@ export const MobileBoardButtons = () => {
<AutoButton /> <AutoButton />
</div> </div>
</> </>
) : isInModQueueView ? (
<>
<ReturnButton
address={subplebbitAddress}
isInAllView={isInAllView}
isInSubscriptionsView={isInSubscriptionsView}
isInModView={isInModView}
isInModQueueView={isInModQueueView}
/>
<RefreshButton />
</>
) : ( ) : (
<> <>
{isInCatalogView ? ( {isInCatalogView ? (
@@ -419,6 +438,7 @@ export const DesktopBoardButtons = () => {
const isInPostView = isPostPageView(location.pathname, params); const isInPostView = isPostPageView(location.pathname, params);
const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams()); const isInSubscriptionsView = isSubscriptionsView(location.pathname, useParams());
const isInModView = isModView(location.pathname); const isInModView = isModView(location.pathname);
const isInModQueueView = isModQueueView(location.pathname);
const { filteredCount, searchText } = useCatalogFiltersStore(); const { filteredCount, searchText } = useCatalogFiltersStore();
@@ -442,6 +462,19 @@ export const DesktopBoardButtons = () => {
<PostPageStats /> <PostPageStats />
</span> </span>
</> </>
) : isInModQueueView ? (
<>
[
<ReturnButton
address={subplebbitAddress}
isInAllView={isInAllView}
isInSubscriptionsView={isInSubscriptionsView}
isInModView={isInModView}
isInModQueueView={isInModQueueView}
/>
] [
<RefreshButton />]
</>
) : ( ) : (
<> <>
{isInCatalogView ? ( {isInCatalogView ? (
@@ -0,0 +1,15 @@
import { useAccountSubplebbits } from '@plebbit/plebbit-react-hooks';
import { useMemo } from 'react';
import { MultisubSubplebbit } from './use-default-subplebbits';
export const useAccountSubplebbitsWithMetadata = (): MultisubSubplebbit[] => {
const { accountSubplebbits } = useAccountSubplebbits();
return useMemo(() => {
return Object.entries(accountSubplebbits || {}).map(([address, sub]) => ({
address,
title: (sub as any).title,
// We can map other fields if needed
}));
}, [accountSubplebbits]);
};
-1
View File
@@ -1,4 +1,3 @@
import { useMemo } from 'react';
import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits';
/** /**
+29
View File
@@ -0,0 +1,29 @@
import {
isAllView,
isBoardView,
isCatalogView,
isHomeView,
isModView,
isModQueueView,
isPendingPostView,
isPostPageView,
isSettingsView,
isSubscriptionsView,
isNotFoundView,
ParamsType,
} from './view-utils';
export {
isAllView,
isBoardView,
isCatalogView,
isHomeView,
isModView,
isModQueueView,
isPendingPostView,
isPostPageView,
isSettingsView,
isSubscriptionsView,
isNotFoundView,
};
export type { ParamsType };
+10
View File
@@ -106,6 +106,7 @@ export const isFeedRoute = (pathname: string): boolean => {
if (normalizedPath.includes('/thread/')) return false; if (normalizedPath.includes('/thread/')) return false;
if (normalizedPath.startsWith('/pending/')) return false; if (normalizedPath.startsWith('/pending/')) return false;
if (normalizedPath.includes('/queue')) return false;
const pathWithoutSettings = normalizedPath.replace(/\/settings$/, ''); const pathWithoutSettings = normalizedPath.replace(/\/settings$/, '');
@@ -137,6 +138,11 @@ export const isPendingPostRoute = (pathname: string): boolean => {
return normalizedPath.startsWith('/pending/'); return normalizedPath.startsWith('/pending/');
}; };
export const isModQueueRoute = (pathname: string): boolean => {
const normalizedPath = pathname.replace(/\/settings$/, '');
return normalizedPath.includes('/queue');
};
export const getFeedCacheKey = (pathname: string): string | null => { export const getFeedCacheKey = (pathname: string): string | null => {
let normalizedPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname; let normalizedPath = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
normalizedPath = normalizedPath.replace(/\/settings$/, ''); normalizedPath = normalizedPath.replace(/\/settings$/, '');
@@ -150,6 +156,10 @@ export const getFeedCacheKey = (pathname: string): string | null => {
return null; return null;
} }
if (normalizedPath.includes('/queue')) {
return null;
}
if (isFeedRoute(pathname)) { if (isFeedRoute(pathname)) {
return normalizedPath; return normalizedPath;
} }
+6 -1
View File
@@ -59,6 +59,10 @@ export const isModView = (pathname: string): boolean => {
return pathname === `/mod` || pathname.startsWith(`/mod/`); return pathname === `/mod` || pathname.startsWith(`/mod/`);
}; };
export const isModQueueView = (pathname: string): boolean => {
return pathname.includes('/queue');
};
export const isPendingPostView = (pathname: string, params: ParamsType): boolean => { export const isPendingPostView = (pathname: string, params: ParamsType): boolean => {
return pathname === `/pending/${params.accountCommentIndex}` || pathname === `/pending/${params.accountCommentIndex}/settings`; return pathname === `/pending/${params.accountCommentIndex}` || pathname === `/pending/${params.accountCommentIndex}/settings`;
}; };
@@ -101,6 +105,7 @@ export const isNotFoundView = (pathname: string, params: ParamsType): boolean =>
!isPendingPostView(pathname, params) && !isPendingPostView(pathname, params) &&
!isPostPageView(pathname, params) && !isPostPageView(pathname, params) &&
!isSettingsView(pathname, params) && !isSettingsView(pathname, params) &&
!isSubscriptionsView(pathname, params) !isSubscriptionsView(pathname, params) &&
!isModQueueView(pathname)
); );
}; };
+25
View File
@@ -0,0 +1,25 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface ModQueueState {
alertThresholdHours: number;
selectedBoardFilter: string | null;
setAlertThresholdHours: (hours: number) => void;
setSelectedBoardFilter: (boardAddress: string | null) => void;
}
const useModQueueStore = create<ModQueueState>()(
persist(
(set) => ({
alertThresholdHours: 6,
selectedBoardFilter: null,
setAlertThresholdHours: (hours) => set({ alertThresholdHours: hours }),
setSelectedBoardFilter: (boardAddress) => set({ selectedBoardFilter: boardAddress }),
}),
{
name: 'mod-queue-storage',
},
),
);
export default useModQueueStore;
+3
View File
@@ -0,0 +1,3 @@
import ModQueueView from './mod-queue';
export default ModQueueView;
+252
View File
@@ -0,0 +1,252 @@
.container {
padding: 10px;
max-width: 1000px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 10px;
}
.title {
font-size: 1.5em;
font-weight: bold;
}
.alertThresholdSetting {
display: flex;
align-items: center;
gap: 8px;
}
.alertThresholdSetting label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
.alertThresholdInput {
width: 50px;
padding: 4px;
border: 1px solid var(--border-color);
background: var(--input-background-color);
color: var(--text-color);
}
.tableHeader {
display: flex;
padding: 8px;
border-bottom: 2px solid var(--border-color);
background: var(--header-background-color);
font-weight: bold;
font-size: 13px;
}
.boardHeader {
width: 50px;
flex-shrink: 0;
}
.excerptHeader {
flex-grow: 1;
}
.timeHeader {
width: 80px;
text-align: right;
flex-shrink: 0;
}
.actionsHeader {
width: 150px;
flex-shrink: 0;
text-align: right;
}
.empty {
padding: 20px;
text-align: center;
color: var(--secondary-text-color);
}
/* ModQueueRow styles */
.row {
display: flex;
padding: 4px 8px;
border-bottom: 1px solid var(--border-color);
align-items: center;
gap: 8px;
background-color: var(--post-background-color);
color: var(--text-color);
font-size: 13px;
}
.row:hover {
background-color: var(--post-highlight-color);
}
.board {
width: 50px;
flex-shrink: 0;
font-weight: bold;
}
.excerpt {
flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.excerpt a {
text-decoration: none;
color: var(--link-color);
}
.excerpt a:hover {
text-decoration: underline;
}
.time {
width: 80px;
flex-shrink: 0;
text-align: right;
color: var(--secondary-text-color);
}
.time.alert {
color: red;
font-weight: bold;
animation: blink 2s infinite;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.actions {
width: 150px;
flex-shrink: 0;
display: flex;
gap: 4px;
justify-content: flex-end;
}
.button {
cursor: pointer;
padding: 2px 6px;
border: 1px solid var(--border-color);
background: var(--button-background);
color: var(--button-text);
font-size: 11px;
text-decoration: none;
display: inline-block;
}
.button:hover {
background: var(--button-hover-background);
}
.approve {
color: green;
}
.reject {
color: red;
}
/* ModQueueBoardFilter styles */
.filterContainer {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.filterSelect {
padding: 4px;
border: 1px solid var(--border-color);
background: var(--input-background-color);
color: var(--text-color);
}
/* ModQueueButton styles */
.modQueueButton {
cursor: pointer;
display: inline-flex;
align-items: center;
margin-left: 8px;
text-decoration: none;
color: var(--text-color);
font-size: 13px;
}
.modQueueButton:hover {
text-decoration: underline;
}
.modQueueButtonCount {
margin-left: 4px;
font-weight: bold;
}
.modQueueButtonAlert {
color: red;
animation: blink 2s infinite;
}
/* Mobile Layout */
@media (max-width: 600px) {
.tableHeader {
display: none;
}
.row {
flex-wrap: wrap;
padding: 8px;
}
.board {
width: auto;
margin-right: 8px;
}
.time {
margin-left: auto;
width: auto;
}
.excerpt {
width: 100%;
margin: 4px 0;
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.actions {
width: 100%;
justify-content: flex-start;
}
.button {
flex: 1;
text-align: center;
padding: 8px;
}
.header {
flex-direction: column;
align-items: flex-start;
}
}
+303
View File
@@ -0,0 +1,303 @@
import React, { useMemo, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, Link } from 'react-router-dom';
import { useAccountSubplebbits, useFeed, Comment, usePublishCommentModeration } from '@plebbit/plebbit-react-hooks';
import { Virtuoso } from 'react-virtuoso';
import { formatDistanceToNow } from 'date-fns';
import styles from './mod-queue.module.css';
import useModQueueStore from '../../stores/use-mod-queue-store';
import { useAccountSubplebbitsWithMetadata } from '../../hooks/use-account-subplebbits-with-metadata';
import LoadingEllipsis from '../../components/loading-ellipsis';
import { useFeedStateString } from '../../hooks/use-state-string';
import { getSubplebbitAddress, getBoardPath } from '../../lib/utils/route-utils';
import { useDefaultSubplebbits, MultisubSubplebbit } from '../../hooks/use-default-subplebbits';
import { useBoardPath } from '../../hooks/use-resolved-subplebbit-address';
import { getHasThumbnail, getCommentMediaInfo } from '../../lib/utils/media-utils';
import useFeedResetStore from '../../stores/use-feed-reset-store';
interface ModQueueViewProps {
boardIdentifier?: string; // If provided, shows queue for single board
}
interface ModQueueRowProps {
comment: Comment;
showBoardColumn?: boolean;
}
const ModQueueRow = ({ comment, showBoardColumn = false }: ModQueueRowProps) => {
const { t } = useTranslation();
const { alertThresholdHours } = useModQueueStore();
const [isModerating, setIsModerating] = useState(false);
const { content, title, timestamp, subplebbitAddress, cid, threadCid, link, thumbnailUrl, linkWidth, linkHeight } = comment;
const boardPath = useBoardPath(subplebbitAddress);
const timeWaiting = Date.now() / 1000 - timestamp;
const isOverThreshold = timeWaiting > alertThresholdHours * 3600;
const { publishCommentModeration: approve } = usePublishCommentModeration({
commentCid: cid,
subplebbitAddress,
commentModeration: { approved: true },
});
const { publishCommentModeration: reject } = usePublishCommentModeration({
commentCid: cid,
subplebbitAddress,
commentModeration: { removed: true },
});
const handleApprove = async () => {
try {
setIsModerating(true);
await approve();
} catch (e) {
console.error(e);
setIsModerating(false);
}
};
const handleReject = async () => {
try {
setIsModerating(true);
await reject();
} catch (e) {
console.error(e);
setIsModerating(false);
}
};
if (isModerating) {
return null; // Or show a loading state / fade out
}
const excerpt = title || content || (getHasThumbnail(getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight), link) ? t('image') : t('no_content'));
const postUrl = `/${boardPath}/thread/${threadCid || cid}`;
return (
<div className={styles.row}>
{showBoardColumn && (
<div className={styles.board}>
<Link to={`/${boardPath}`}>/{boardPath}/</Link>
</div>
)}
<div className={styles.excerpt}>
<Link to={postUrl} title={excerpt}>
{excerpt}
</Link>
</div>
<div className={`${styles.time} ${isOverThreshold ? styles.alert : ''}`}>{formatDistanceToNow(timestamp * 1000, { addSuffix: false })}</div>
<div className={styles.actions}>
<button className={`${styles.button} ${styles.approve}`} onClick={handleApprove}>
{t('approve')}
</button>
<button className={`${styles.button} ${styles.reject}`} onClick={handleReject}>
{t('reject')}
</button>
<Link to={postUrl} className={styles.button}>
{t('view')}
</Link>
</div>
</div>
);
};
interface ModQueueBoardFilterProps {
subplebbits: MultisubSubplebbit[];
}
const ModQueueBoardFilter = ({ subplebbits }: ModQueueBoardFilterProps) => {
const { t } = useTranslation();
const { selectedBoardFilter, setSelectedBoardFilter } = useModQueueStore();
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
setSelectedBoardFilter(value === '' ? null : value);
};
if (!subplebbits || subplebbits.length === 0) {
return null;
}
return (
<div className={styles.filterContainer}>
<label>{t('filter_by_board')}:</label>
<select className={styles.filterSelect} value={selectedBoardFilter || ''} onChange={handleChange}>
<option value=''>{t('all_boards')}</option>
{subplebbits.map((sub) => {
const address = sub.address;
if (!address) return null;
return (
<option key={address} value={address}>
/{getBoardPath(address, subplebbits)}/
</option>
);
})}
</select>
</div>
);
};
interface ModQueueButtonProps {
boardIdentifier?: string;
isMobile?: boolean;
}
export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProps) => {
const { t } = useTranslation();
const { alertThresholdHours } = useModQueueStore();
const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = useMemo(() => Object.keys(accountSubplebbits || {}), [accountSubplebbits]);
const subplebbitAddresses = useMemo(() => {
if (boardIdentifier) {
return [boardIdentifier];
}
return accountSubplebbitAddresses;
}, [boardIdentifier, accountSubplebbitAddresses]);
// If specific board, check if user is mod
const isModOfBoard = boardIdentifier ? accountSubplebbitAddresses.includes(boardIdentifier) : true;
// Only fetch if we have addresses to check and permissions
const shouldFetch = subplebbitAddresses.length > 0 && isModOfBoard;
const { feed } = useFeed({
subplebbitAddresses: shouldFetch ? subplebbitAddresses : [],
modQueue: ['pendingApproval'],
postsPerPage: 100, // Fetch enough to check timestamps
});
if (!shouldFetch || subplebbitAddresses.length === 0) {
return null;
}
const count = feed.length;
const hasAlert = feed.some((item) => {
const timeWaiting = Date.now() / 1000 - item.timestamp;
return timeWaiting > alertThresholdHours * 3600;
});
const to = boardIdentifier ? `/${boardIdentifier}/queue` : '/mod/queue';
if (count === 0) {
// If we are on a specific board and the user is a mod, we still show the button
// If we are in global mod view, we show it too
return (
<Link to={to} className={styles.modQueueButton}>
[{t('mod_queue')}]
</Link>
);
}
return (
<Link to={to} className={`${styles.modQueueButton} ${hasAlert ? styles.modQueueButtonAlert : ''}`}>
[{t('mod_queue')} <span className={styles.modQueueButtonCount}>{count}</span>]
</Link>
);
};
export const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
const { t } = useTranslation();
const params = useParams();
const { selectedBoardFilter, alertThresholdHours, setAlertThresholdHours } = useModQueueStore();
const { accountSubplebbits } = useAccountSubplebbits();
const accountSubplebbitAddresses = Object.keys(accountSubplebbits);
const defaultSubplebbits = useDefaultSubplebbits();
const boardIdentifier = propBoardIdentifier || params.boardIdentifier;
// Resolve boardIdentifier to address if it exists
const resolvedAddress = useMemo(() => {
if (boardIdentifier) {
return getSubplebbitAddress(boardIdentifier, defaultSubplebbits);
}
return undefined;
}, [boardIdentifier, defaultSubplebbits]);
// Get metadata for filter dropdown
const subplebbitsWithMetadata = useAccountSubplebbitsWithMetadata();
const subplebbitAddresses = useMemo(() => {
if (resolvedAddress) {
return [resolvedAddress];
}
if (selectedBoardFilter) {
return [selectedBoardFilter];
}
return accountSubplebbitAddresses;
}, [resolvedAddress, selectedBoardFilter, accountSubplebbitAddresses]);
const { feed, hasMore, loadMore, reset } = useFeed({
subplebbitAddresses,
modQueue: ['pendingApproval'],
postsPerPage: 50,
});
// Register reset function with feed reset store so refresh button works
const setResetFunction = useFeedResetStore((state) => state.setResetFunction);
useEffect(() => {
setResetFunction(reset);
}, [reset, setResetFunction]);
const loadingStateString = useFeedStateString(subplebbitAddresses) || t('loading');
const showBoardColumn = !resolvedAddress && !selectedBoardFilter;
const Footer = () => {
return hasMore ? (
<div style={{ padding: '10px', textAlign: 'center' }}>
<LoadingEllipsis string={loadingStateString} />
</div>
) : null;
};
return (
<div className={styles.container}>
<div className={styles.header}>
<div className={styles.title}>{t('moderation_queue')}</div>
<div className={styles.alertThresholdSetting}>
<label>
{t('mod_queue_alert_threshold')}:
<input
type='number'
min='1'
value={alertThresholdHours}
onChange={(e) => setAlertThresholdHours(Number(e.target.value))}
className={styles.alertThresholdInput}
/>{' '}
{t('hours')}
</label>
</div>
</div>
{!resolvedAddress && <ModQueueBoardFilter subplebbits={subplebbitsWithMetadata} />}
{feed.length === 0 && !hasMore ? (
<div className={styles.empty}>{t('queue_is_empty')}</div>
) : (
<>
<div className={styles.tableHeader}>
{showBoardColumn && <div className={styles.boardHeader}>{t('board')}</div>}
<div className={styles.excerptHeader}>{t('excerpt')}</div>
<div className={styles.timeHeader}>{t('waiting')}</div>
<div className={styles.actionsHeader}>{t('actions')}</div>
</div>
<Virtuoso
useWindowScroll
data={feed}
totalCount={feed.length}
endReached={loadMore}
itemContent={(index, comment) => <ModQueueRow key={comment.cid} comment={comment} showBoardColumn={showBoardColumn} />}
components={{ Footer }}
/>
</>
)}
</div>
);
};
export default ModQueueView;