mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
+1
-1
@@ -11,7 +11,7 @@
|
||||
"@capacitor/status-bar": "7.0.1",
|
||||
"@capawesome/capacitor-android-edge-to-edge-support": "7.2.2",
|
||||
"@floating-ui/react": "0.26.1",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#0bfd516d2bfae47bfc255988a2e845f849adcf12",
|
||||
"@plebbit/plebbit-react-hooks": "https://github.com/plebbit/plebbit-react-hooks.git#a828bd598bcc392ed379136feaee0a7b1c8cd23f",
|
||||
"@react-spring/web": "9.7.5",
|
||||
"@types/node": "20.8.2",
|
||||
"@types/react": "18.2.25",
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "متابعة",
|
||||
"loading_editor": "جاري تحميل المحرر...",
|
||||
"editor_fallback_warning": "فشل تحميل محرر الأكواد. يتم استخدام حقل نص عادي بدلاً منه.",
|
||||
"return_to_settings": "العودة إلى الإعدادات"
|
||||
"return_to_settings": "العودة إلى الإعدادات",
|
||||
"import_account_backup": "استيراد نسخة احتياطية للحساب",
|
||||
"account_auto_generated": "تم إنشاء هذا الحساب تلقائياً بواسطة 5chan، دون الحاجة للتسجيل.",
|
||||
"download_backup": "تحميل النسخة الاحتياطية",
|
||||
"boards_you_moderate_nav": "المنتديات التي تقوم بالإشراف عليها"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "চালিয়ে যান",
|
||||
"loading_editor": "এডিটর লোড হচ্ছে...",
|
||||
"editor_fallback_warning": "কোড এডিটর লোড করতে ব্যর্থ হয়েছে। পরিবর্তে একটি সাধারণ টেক্সট এরিয়া ব্যবহার করা হচ্ছে।",
|
||||
"return_to_settings": "সেটিংসে ফিরে যান"
|
||||
"return_to_settings": "সেটিংসে ফিরে যান",
|
||||
"import_account_backup": "অ্যাকাউন্ট ব্যাকআপ আমদানি করুন",
|
||||
"account_auto_generated": "এই অ্যাকাউন্টটি 5chan দ্বারা স্বয়ংক্রিয়ভাবে তৈরি হয়েছে, সাইনআপের প্রয়োজন নেই।",
|
||||
"download_backup": "ব্যাকআপ ডাউনলোড করুন",
|
||||
"boards_you_moderate_nav": "আপনি যা মডারেট করেন এমন বোর্ড"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Pokračovat",
|
||||
"loading_editor": "Načítání editoru...",
|
||||
"editor_fallback_warning": "Načtení editoru kódu selhalo. Místo toho se používá jednoduché textové pole.",
|
||||
"return_to_settings": "Zpět na nastavení"
|
||||
"return_to_settings": "Zpět na nastavení",
|
||||
"import_account_backup": "Importovat zálohu účtu",
|
||||
"account_auto_generated": "Tento účet byl automaticky vygenerován 5chanem, registrace není nutná.",
|
||||
"download_backup": "Stáhnout zálohu",
|
||||
"boards_you_moderate_nav": "Fóra, která moderujete"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Fortsæt",
|
||||
"loading_editor": "Indlæser editor...",
|
||||
"editor_fallback_warning": "Kodeeditoren kunne ikke indlæses. Bruger et simpelt tekstfelt i stedet.",
|
||||
"return_to_settings": "Tilbage til indstillinger"
|
||||
"return_to_settings": "Tilbage til indstillinger",
|
||||
"import_account_backup": "Importer kontobackup",
|
||||
"account_auto_generated": "Denne konto blev automatisk oprettet af 5chan, ingen tilmelding påkrævet.",
|
||||
"download_backup": "Download sikkerhedskopi",
|
||||
"boards_you_moderate_nav": "Boards du modererer"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Weiter",
|
||||
"loading_editor": "Editor wird geladen...",
|
||||
"editor_fallback_warning": "Der Code-Editor konnte nicht geladen werden. Es wird stattdessen ein einfaches Textfeld verwendet.",
|
||||
"return_to_settings": "Zurück zu den Einstellungen"
|
||||
"return_to_settings": "Zurück zu den Einstellungen",
|
||||
"import_account_backup": "Kontosicherung importieren",
|
||||
"account_auto_generated": "Dieses Konto wurde automatisch von 5chan erstellt, keine Anmeldung erforderlich.",
|
||||
"download_backup": "Sicherung herunterladen",
|
||||
"boards_you_moderate_nav": "Von dir moderierte Boards"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Συνέχεια",
|
||||
"loading_editor": "Φόρτωση του επεξεργαστή...",
|
||||
"editor_fallback_warning": "Αποτυχία φόρτωσης του επεξεργαστή κώδικα. Χρησιμοποιείται απλό πεδίο κειμένου αντί για αυτό.",
|
||||
"return_to_settings": "Επιστροφή στις Ρυθμίσεις"
|
||||
"return_to_settings": "Επιστροφή στις Ρυθμίσεις",
|
||||
"import_account_backup": "Εισαγωγή αντιγράφου ασφαλείας λογαριασμού",
|
||||
"account_auto_generated": "Αυτός ο λογαριασμός δημιουργήθηκε αυτόματα από το 5chan, χωρίς απαιτούμενη εγγραφή.",
|
||||
"download_backup": "Λήψη αντιγράφου ασφαλείας",
|
||||
"boards_you_moderate_nav": "Πίνακες που διαχειρίζεστε"
|
||||
}
|
||||
|
||||
@@ -290,5 +290,9 @@
|
||||
"continue": "Continue",
|
||||
"loading_editor": "Loading editor...",
|
||||
"editor_fallback_warning": "The code editor failed to load. Using a plain textarea instead.",
|
||||
"return_to_settings": "Return to Settings"
|
||||
"return_to_settings": "Return to Settings",
|
||||
"account_auto_generated": "This account was automatically generated by 5chan, no signup required.",
|
||||
"download_backup": "Download Backup",
|
||||
"import_account_backup": "Import Account Backup",
|
||||
"boards_you_moderate_nav": "Boards You Moderate"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Continuar",
|
||||
"loading_editor": "Cargando editor...",
|
||||
"editor_fallback_warning": "No se pudo cargar el editor de código. Se usa un campo de texto simple en su lugar.",
|
||||
"return_to_settings": "Volver a Configuración"
|
||||
"return_to_settings": "Volver a Configuración",
|
||||
"import_account_backup": "Importar copia de seguridad de la cuenta",
|
||||
"account_auto_generated": "Esta cuenta fue generada automáticamente por 5chan, no se requiere registro.",
|
||||
"download_backup": "Descargar copia de seguridad",
|
||||
"boards_you_moderate_nav": "Tableros que moderas"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "ادامه",
|
||||
"loading_editor": "در حال بارگذاری ویرایشگر...",
|
||||
"editor_fallback_warning": "بارگذاری ویرایشگر کد ناموفق بود. در عوض از یک فیلد متنی ساده استفاده میشود.",
|
||||
"return_to_settings": "بازگشت به تنظیمات"
|
||||
"return_to_settings": "بازگشت به تنظیمات",
|
||||
"import_account_backup": "وارد کردن پشتیبان حساب",
|
||||
"account_auto_generated": "این حساب بهطور خودکار توسط 5chan ایجاد شده، نیازی به ثبتنام نیست.",
|
||||
"download_backup": "دانلود پشتیبان",
|
||||
"boards_you_moderate_nav": "تابلوهایی که مدیریت میکنید"
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"help": "Apua",
|
||||
"filter_and_highlights_help": "Suodattimet ja korostukset -apu",
|
||||
"filter_and_highlights": "Suodattimet ja korostukset",
|
||||
"boards_you_moderate": "lautakunnat, joita valvot",
|
||||
"boards_you_moderate": "Laudat, joita moderoit",
|
||||
"moderator_of_this_board": "Tämä käyttäjä on tämän taulun moderaattori",
|
||||
"administrator_of_this_board": "Tämä käyttäjä on tämän taulun järjestelmänvalvoja",
|
||||
"copy_content_id": "kopioi sisältö-ID",
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Jatka",
|
||||
"loading_editor": "Ladataan editoria...",
|
||||
"editor_fallback_warning": "Koodieditorin lataus epäonnistui. Käytetään sen sijaan yksinkertaista tekstikenttää.",
|
||||
"return_to_settings": "Palaa asetuksiin"
|
||||
"return_to_settings": "Palaa asetuksiin",
|
||||
"import_account_backup": "Tuo tilin varmuuskopio",
|
||||
"account_auto_generated": "Tämä tili luotiin automaattisesti 5chanissa, rekisteröitymistä ei tarvita.",
|
||||
"download_backup": "Lataa varmuuskopio",
|
||||
"boards_you_moderate_nav": "Laudat, joita moderoit"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Magpatuloy",
|
||||
"loading_editor": "Nilo-load ang editor...",
|
||||
"editor_fallback_warning": "Nabigo ang pag-load ng code editor. Gumagamit ng plain textarea sa halip.",
|
||||
"return_to_settings": "Bumalik sa Mga Setting"
|
||||
"return_to_settings": "Bumalik sa Mga Setting",
|
||||
"import_account_backup": "I-import ang Backup ng Account",
|
||||
"account_auto_generated": "Ang account na ito ay awtomatikong nilikha ng 5chan, walang kailangang signup.",
|
||||
"download_backup": "I-download ang Backup",
|
||||
"boards_you_moderate_nav": "Mga board na minomodera mo"
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"help": "Aide",
|
||||
"filter_and_highlights_help": "Aide sur les filtres et surlignages",
|
||||
"filter_and_highlights": "Filtres et surlignages",
|
||||
"boards_you_moderate": "boards que vous modérez",
|
||||
"boards_you_moderate": "Tableaux que vous modérez",
|
||||
"moderator_of_this_board": "Cet utilisateur est modérateur de ce tableau",
|
||||
"administrator_of_this_board": "Cet utilisateur est administrateur de ce tableau",
|
||||
"copy_content_id": "Copier l'ID du contenu",
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Continuer",
|
||||
"loading_editor": "Chargement de l'éditeur...",
|
||||
"editor_fallback_warning": "Le chargement de l'éditeur de code a échoué. Utilisation d'un champ de texte simple à la place.",
|
||||
"return_to_settings": "Retour aux paramètres"
|
||||
"return_to_settings": "Retour aux paramètres",
|
||||
"import_account_backup": "Importer la sauvegarde du compte",
|
||||
"account_auto_generated": "Ce compte a été généré automatiquement par 5chan, aucune inscription requise.",
|
||||
"download_backup": "Télécharger la sauvegarde",
|
||||
"boards_you_moderate_nav": "Tableaux que vous modérez"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "המשך",
|
||||
"loading_editor": "טוען עורך...",
|
||||
"editor_fallback_warning": "טעינת עורך הקוד נכשלה. משתמשים בשדה טקסט פשוט במקום.",
|
||||
"return_to_settings": "חזרה להגדרות"
|
||||
"return_to_settings": "חזרה להגדרות",
|
||||
"import_account_backup": "ייבוא גיבוי חשבון",
|
||||
"account_auto_generated": "חשבון זה נוצר אוטומטית על ידי 5chan, אין צורך בהרשמה.",
|
||||
"download_backup": "הורד גיבוי",
|
||||
"boards_you_moderate_nav": "לוחות שאתה מפקח עליהם"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "जारी रखें",
|
||||
"loading_editor": "संपादक लोड हो रहा है...",
|
||||
"editor_fallback_warning": "कोड एडिटर लोड नहीं हो सका। इसके बजाय एक सामान्य टेक्स्ट एरिया का उपयोग किया जा रहा है।",
|
||||
"return_to_settings": "सेटिंग्स पर वापस जाएं"
|
||||
"return_to_settings": "सेटिंग्स पर वापस जाएं",
|
||||
"import_account_backup": "खाता बैकअप आयात करें",
|
||||
"account_auto_generated": "यह खाता 5chan द्वारा स्वचालित रूप से बनाया गया था, साइनअप की आवश्यकता नहीं है।",
|
||||
"download_backup": "बैकअप डाउनलोड करें",
|
||||
"boards_you_moderate_nav": "बोर्ड जिन्हें आप मॉडरेट करते हैं"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Folytatás",
|
||||
"loading_editor": "Szerkesztő betöltése...",
|
||||
"editor_fallback_warning": "A kódszerkesztő betöltése sikertelen. Egyszerű szövegmező használata helyette.",
|
||||
"return_to_settings": "Vissza a beállításokhoz"
|
||||
"return_to_settings": "Vissza a beállításokhoz",
|
||||
"import_account_backup": "Fiók biztonsági mentés importálása",
|
||||
"account_auto_generated": "Ezt a fiókot a 5chan automatikusan hozta létre, regisztráció nem szükséges.",
|
||||
"download_backup": "Biztonsági mentés letöltése",
|
||||
"boards_you_moderate_nav": "A táblák, amelyeket moderálsz"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Lanjutkan",
|
||||
"loading_editor": "Memuat editor...",
|
||||
"editor_fallback_warning": "Editor kode gagal dimuat. Menggunakan textarea biasa sebagai gantinya.",
|
||||
"return_to_settings": "Kembali ke Pengaturan"
|
||||
"return_to_settings": "Kembali ke Pengaturan",
|
||||
"import_account_backup": "Impor Cadangan Akun",
|
||||
"account_auto_generated": "Akun ini dibuat secara otomatis oleh 5chan, tidak perlu mendaftar.",
|
||||
"download_backup": "Unduh cadangan",
|
||||
"boards_you_moderate_nav": "Papan yang Anda moderasi"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Continua",
|
||||
"loading_editor": "Caricamento editor...",
|
||||
"editor_fallback_warning": "Il caricamento dell'editor di codice non è riuscito. Si usa un campo di testo semplice al suo posto.",
|
||||
"return_to_settings": "Torna alle impostazioni"
|
||||
"return_to_settings": "Torna alle impostazioni",
|
||||
"import_account_backup": "Importa backup account",
|
||||
"account_auto_generated": "Questo account è stato generato automaticamente da 5chan, nessuna registrazione richiesta.",
|
||||
"download_backup": "Scarica backup",
|
||||
"boards_you_moderate_nav": "Board che moderi"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "続ける",
|
||||
"loading_editor": "エディターを読み込み中...",
|
||||
"editor_fallback_warning": "コードエディターの読み込みに失敗しました。代わりにプレーンテキストエリアを使用します。",
|
||||
"return_to_settings": "設定に戻る"
|
||||
"return_to_settings": "設定に戻る",
|
||||
"import_account_backup": "アカウントバックアップをインポート",
|
||||
"account_auto_generated": "このアカウントは5chanによって自動生成されました。サインアップは不要です。",
|
||||
"download_backup": "バックアップをダウンロード",
|
||||
"boards_you_moderate_nav": "あなたがモデレートする板"
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"help": "도움말",
|
||||
"filter_and_highlights_help": "필터 및 하이라이트 도움말",
|
||||
"filter_and_highlights": "필터 및 하이라이트",
|
||||
"boards_you_moderate": "당신이 관리하는 게시판",
|
||||
"boards_you_moderate": "관리하는 게시판",
|
||||
"moderator_of_this_board": "이 사용자는 이 게시판의 관리자입니다",
|
||||
"administrator_of_this_board": "이 사용자는 이 게시판의 관리자입니다",
|
||||
"copy_content_id": "콘텐츠 ID 복사",
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "계속",
|
||||
"loading_editor": "편집기 로딩 중...",
|
||||
"editor_fallback_warning": "코드 편집기 로드에 실패했습니다. 대신 일반 텍스트 영역을 사용합니다.",
|
||||
"return_to_settings": "설정으로 돌아가기"
|
||||
"return_to_settings": "설정으로 돌아가기",
|
||||
"import_account_backup": "계정 백업 가져오기",
|
||||
"account_auto_generated": "이 계정은 5chan에서 자동으로 생성되었으며, 가입이 필요하지 않습니다.",
|
||||
"download_backup": "백업 다운로드",
|
||||
"boards_you_moderate_nav": "당신이 관리하는 게시판"
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
"pending": "प्रलंबित",
|
||||
"failed": "असफल",
|
||||
"add": "अॅड करा",
|
||||
"filtered_threads": "फिल्टर केलेले ध糹रे",
|
||||
"filtered_threads": "फिल्टर केलेले धागे",
|
||||
"missing_link_confirm": "तुमचा पोस्ट डिफॉल्टनुसार 5chan वर दृश्यमान होणार नाही, कारण त्यात चित्र किंवा इतर मीडियाची लिंक नाही. तुम्ही सेटिंग्जमध्ये हा फिल्टर अक्षम करू शकता।",
|
||||
"no_board_selected_warning": "पोस्ट करण्यापूर्वी कृपया एक बोर्ड निवडा.",
|
||||
"invalid_url_alert": "दिलेला लिंक वैध URL नाही.",
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "सुरू ठेवा",
|
||||
"loading_editor": "संपादक लोड होत आहे...",
|
||||
"editor_fallback_warning": "कोड एडिटर लोड नाही झाला. त्याऐवजी साध्या टेक्स्ट एरियाचा वापर केला जात आहे.",
|
||||
"return_to_settings": "सेटिंग्जवर परत जा"
|
||||
"return_to_settings": "सेटिंग्जवर परत जा",
|
||||
"import_account_backup": "खाते बॅकअप आयात करा",
|
||||
"account_auto_generated": "हा खाता 5chan द्वारा स्वयंचलितपणे तयार केला गेला, साइनअप आवश्यक नाही.",
|
||||
"download_backup": "बॅकअप डाउनलोड करा",
|
||||
"boards_you_moderate_nav": "तुम्ही मॉडरेट करता ते बोर्ड"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Doorgaan",
|
||||
"loading_editor": "Editor laden...",
|
||||
"editor_fallback_warning": "De code-editor kon niet worden geladen. Er wordt een eenvoudig tekstveld gebruikt.",
|
||||
"return_to_settings": "Terug naar Instellingen"
|
||||
"return_to_settings": "Terug naar Instellingen",
|
||||
"import_account_backup": "Accountback-up importeren",
|
||||
"account_auto_generated": "Dit account is automatisch gegenereerd door 5chan, geen aanmelding vereist.",
|
||||
"download_backup": "Back-up downloaden",
|
||||
"boards_you_moderate_nav": "Boards die je beheert"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Fortsett",
|
||||
"loading_editor": "Laster editor...",
|
||||
"editor_fallback_warning": "Kodeeditoren kunne ikke lastes. Bruker et enkelt tekstfelt i stedet.",
|
||||
"return_to_settings": "Tilbake til Innstillinger"
|
||||
"return_to_settings": "Tilbake til Innstillinger",
|
||||
"import_account_backup": "Importer kontobackup",
|
||||
"account_auto_generated": "Denne kontoen ble automatisk opprettet av 5chan, ingen registrering nødvendig.",
|
||||
"download_backup": "Last ned sikkerhetskopi",
|
||||
"boards_you_moderate_nav": "Boards du modererer"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Kontynuuj",
|
||||
"loading_editor": "Ładowanie edytora...",
|
||||
"editor_fallback_warning": "Nie udało się załadować edytora kodu. Używane jest zamiast tego prostego pola tekstowego.",
|
||||
"return_to_settings": "Powrót do ustawień"
|
||||
"return_to_settings": "Powrót do ustawień",
|
||||
"import_account_backup": "Importuj kopię zapasową konta",
|
||||
"account_auto_generated": "To konto zostało automatycznie wygenerowane przez 5chan, rejestracja nie jest wymagana.",
|
||||
"download_backup": "Pobierz kopię zapasową",
|
||||
"boards_you_moderate_nav": "Tablice, które moderujesz"
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
"help": "Ajuda",
|
||||
"filter_and_highlights_help": "Ajuda de Filtros e Realces",
|
||||
"filter_and_highlights": "Filtros e Destaques",
|
||||
"boards_you_moderate": "boards que você modera",
|
||||
"boards_you_moderate": "Quadros que você modera",
|
||||
"moderator_of_this_board": "Este usuário é moderador deste quadro",
|
||||
"administrator_of_this_board": "Este usuário é administrador deste quadro",
|
||||
"copy_content_id": "Copiar ID do conteúdo",
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Continuar",
|
||||
"loading_editor": "Carregando editor...",
|
||||
"editor_fallback_warning": "O editor de código falhou ao carregar. Usando um campo de texto simples em vez disso.",
|
||||
"return_to_settings": "Voltar às Configurações"
|
||||
"return_to_settings": "Voltar às Configurações",
|
||||
"import_account_backup": "Importar backup da conta",
|
||||
"account_auto_generated": "Esta conta foi gerada automaticamente pelo 5chan, sem necessidade de cadastro.",
|
||||
"download_backup": "Baixar backup",
|
||||
"boards_you_moderate_nav": "Quadros que você modera"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Continuă",
|
||||
"loading_editor": "Se încarcă editorul...",
|
||||
"editor_fallback_warning": "Editorul de cod nu s-a putut încărca. Se folosește un câmp de text simplu în loc.",
|
||||
"return_to_settings": "Înapoi la Setări"
|
||||
"return_to_settings": "Înapoi la Setări",
|
||||
"import_account_backup": "Importă backup cont",
|
||||
"account_auto_generated": "Acest cont a fost generat automat de 5chan, nu este nevoie de înregistrare.",
|
||||
"download_backup": "Descarcă backup",
|
||||
"boards_you_moderate_nav": "Forumuri pe care le moderezi"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Продолжить",
|
||||
"loading_editor": "Загрузка редактора...",
|
||||
"editor_fallback_warning": "Не удалось загрузить редактор кода. Используется вместо него обычное текстовое поле.",
|
||||
"return_to_settings": "Вернуться к настройкам"
|
||||
"return_to_settings": "Вернуться к настройкам",
|
||||
"import_account_backup": "Импорт резервной копии аккаунта",
|
||||
"account_auto_generated": "Этот аккаунт был автоматически создан 5chan, регистрация не требуется.",
|
||||
"download_backup": "Скачать резервную копию",
|
||||
"boards_you_moderate_nav": "Доски, которые вы модерируете"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Vazhdo",
|
||||
"loading_editor": "Duke ngarkuar redaktuesin...",
|
||||
"editor_fallback_warning": "Redaktori i kodit dështoi të ngarkohet. Duke përdorur një fushë teksti të thjeshtë në vend.",
|
||||
"return_to_settings": "Kthehu te Cilësimet"
|
||||
"return_to_settings": "Kthehu te Cilësimet",
|
||||
"import_account_backup": "Importo backup të llogarisë",
|
||||
"account_auto_generated": "Ky llogari u krijua automatikisht nga 5chan, nuk kërkohet regjistrim.",
|
||||
"download_backup": "Shkarko kopjen e rezervë",
|
||||
"boards_you_moderate_nav": "Bordet që moderoni"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Fortsätt",
|
||||
"loading_editor": "Laddar editor...",
|
||||
"editor_fallback_warning": "Kodredigeraren kunde inte laddas. Använder ett enkelt textfält istället.",
|
||||
"return_to_settings": "Tillbaka till inställningar"
|
||||
"return_to_settings": "Tillbaka till inställningar",
|
||||
"import_account_backup": "Importera kontobackup",
|
||||
"account_auto_generated": "Detta konto skapades automatiskt av 5chan, ingen registrering krävs.",
|
||||
"download_backup": "Ladda ner säkerhetskopia",
|
||||
"boards_you_moderate_nav": "Boards du modererar"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "కొనసాగించు",
|
||||
"loading_editor": "సంపాదకాన్ని లోడ్ చేస్తోంది...",
|
||||
"editor_fallback_warning": "కోడ్ ఎడిటర్ లోడ్ చేయడం విఫలమైంది. బదులుగా సాధారణ టెక్స్ట్ ఏరియా ఉపయోగిస్తున్నారు.",
|
||||
"return_to_settings": "సెట్టింగ్లకు తిరిగి వెళ్ళండి"
|
||||
"return_to_settings": "సెట్టింగ్లకు తిరిగి వెళ్ళండి",
|
||||
"import_account_backup": "ఖాతా బ్యాకప్ దిగుమతి చేయండి",
|
||||
"account_auto_generated": "ఈ ఖాతా 5chan చేత స్వయంచాలకంగా సృష్టించబడింది, సైన్ అప్ అవసరం లేదు.",
|
||||
"download_backup": "బ్యాకప్ డౌన్లోడ్ చేయండి",
|
||||
"boards_you_moderate_nav": "మీరు మోడరేట్ చేసే బోర్డ్లు"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "ดำเนินการต่อ",
|
||||
"loading_editor": "กำลังโหลดตัวแก้ไข...",
|
||||
"editor_fallback_warning": "โหลดตัวแก้ไขโค้ดไม่สำเร็จ กำลังใช้ช่องข้อความธรรมดาแทน",
|
||||
"return_to_settings": "กลับไปที่การตั้งค่า"
|
||||
"return_to_settings": "กลับไปที่การตั้งค่า",
|
||||
"import_account_backup": "นำเข้ารายการสำรองของบัญชี",
|
||||
"account_auto_generated": "บัญชีนี้ถูกสร้างโดยอัตโนมัติโดย 5chan ไม่ต้องสมัครสมาชิก",
|
||||
"download_backup": "ดาวน์โหลดสำรอง",
|
||||
"boards_you_moderate_nav": "บอร์ดที่คุณดูแล"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Devam",
|
||||
"loading_editor": "Düzenleyici yükleniyor...",
|
||||
"editor_fallback_warning": "Kod düzenleyici yüklenemedi. Bunun yerine düz bir metin alanı kullanılıyor.",
|
||||
"return_to_settings": "Ayarlara Dön"
|
||||
"return_to_settings": "Ayarlara Dön",
|
||||
"import_account_backup": "Hesap yedeğini içe aktar",
|
||||
"account_auto_generated": "Bu hesap 5chan tarafından otomatik olarak oluşturuldu, kayıt gerekmez.",
|
||||
"download_backup": "Yedek indir",
|
||||
"boards_you_moderate_nav": "Yönettiğiniz panolar"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Продовжити",
|
||||
"loading_editor": "Завантаження редактора...",
|
||||
"editor_fallback_warning": "Не вдалося завантажити редактор коду. Використовується прості текстове поле замість нього.",
|
||||
"return_to_settings": "Повернутися до налаштувань"
|
||||
"return_to_settings": "Повернутися до налаштувань",
|
||||
"import_account_backup": "Імпорт резервної копії облікового запису",
|
||||
"account_auto_generated": "Цей обліковий запис було автоматично створено 5chan, реєстрація не потрібна.",
|
||||
"download_backup": "Завантажити резервну копію",
|
||||
"boards_you_moderate_nav": "Дошки, які ви модеруєте"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "جاری رکھیں",
|
||||
"loading_editor": "ایڈیٹر لوڈ ہو رہا ہے...",
|
||||
"editor_fallback_warning": "کوڈ ایڈیٹر لوڈ نہیں ہو سکا۔ اس کے بجائے ایک سادہ ٹیکسٹ ایریا استعمال کیا جا رہا ہے۔",
|
||||
"return_to_settings": "ترتیبات میں واپس جائیں"
|
||||
"return_to_settings": "ترتیبات میں واپس جائیں",
|
||||
"import_account_backup": "اکاؤنٹ بیک اپ درآمد کریں",
|
||||
"account_auto_generated": "یہ اکاؤنٹ 5chan نے خودکار طور پر بنایا ہے، سائن اپ کی ضرورت نہیں۔",
|
||||
"download_backup": "بیک اپ ڈاؤن لوڈ کریں",
|
||||
"boards_you_moderate_nav": "بورڈز جنہیں آپ ماڈریٹ کرتے ہیں"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "Tiếp tục",
|
||||
"loading_editor": "Đang tải trình soạn thảo...",
|
||||
"editor_fallback_warning": "Không thể tải trình soạn thảo mã. Đang sử dụng ô văn bản thuần thay thế.",
|
||||
"return_to_settings": "Quay lại Cài đặt"
|
||||
"return_to_settings": "Quay lại Cài đặt",
|
||||
"import_account_backup": "Nhập bản sao lưu tài khoản",
|
||||
"account_auto_generated": "Tài khoản này được tạo tự động bởi 5chan, không cần đăng ký.",
|
||||
"download_backup": "Tải xuống bản sao lưu",
|
||||
"boards_you_moderate_nav": "Các bảng bạn điều hành"
|
||||
}
|
||||
|
||||
@@ -291,5 +291,9 @@
|
||||
"continue": "继续",
|
||||
"loading_editor": "正在加载编辑器...",
|
||||
"editor_fallback_warning": "代码编辑器加载失败。改用纯文本输入框。",
|
||||
"return_to_settings": "返回设置"
|
||||
"return_to_settings": "返回设置",
|
||||
"import_account_backup": "导入账户备份",
|
||||
"account_auto_generated": "此账户由 5chan 自动生成,无需注册。",
|
||||
"download_backup": "下载备份",
|
||||
"boards_you_moderate_nav": "您管理的板块"
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ export const PostPageStats = () => {
|
||||
const subplebbitAddress = resolvedAddress || accountComment?.subplebbitAddress;
|
||||
|
||||
const comment = useComment({ commentCid });
|
||||
const postCid = comment?.parentCid ?? commentCid;
|
||||
const postCid = comment?.postCid ?? commentCid;
|
||||
const post = useComment({ commentCid: postCid });
|
||||
|
||||
const { closed, pinned, replyCount } = post || {};
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
overflow: hidden;
|
||||
list-style: none;
|
||||
background-color: var(--pagelist-background-color);
|
||||
border-right: 1px solid var(--pagelist-item-border-color);
|
||||
border-bottom: 1px solid var(--pagelist-item-border-color);
|
||||
}
|
||||
|
||||
.footerPageItem {
|
||||
@@ -97,7 +99,7 @@
|
||||
background: transparent;
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
color: var(--boardsbar-desktop-link-text-color);
|
||||
color: var(--button-desktop-text-color);
|
||||
}
|
||||
|
||||
.footerPageLink {
|
||||
@@ -108,19 +110,24 @@
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--boardsbar-desktop-link-text-color);
|
||||
color: var(--button-desktop-text-color);
|
||||
text-transform: capitalize;
|
||||
cursor: pointer;
|
||||
text-decoration: var(--boardsbar-desktop-link-text-decoration);
|
||||
text-decoration: var(--button-text-decoration);
|
||||
}
|
||||
|
||||
.footerPageLink:hover {
|
||||
color: var(--boardsbar-desktop-link-text-color-hover);
|
||||
text-decoration: var(--boardsbar-desktop-link-text-decoration-hover);
|
||||
color: var(--button-desktop-text-color-hover);
|
||||
}
|
||||
|
||||
.pagelistSeparatorLink {
|
||||
composes: footerPageLink;
|
||||
border-left: 1px solid var(--pagelist-item-border-color);
|
||||
padding-left: 12px;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.footerPageCurrent {
|
||||
composes: footerPageLink;
|
||||
font-weight: bold;
|
||||
color: var(--boardsbar-desktop-link-text-color-hover);
|
||||
text-decoration: var(--boardsbar-desktop-link-text-decoration);
|
||||
}
|
||||
|
||||
@@ -61,23 +61,15 @@ const BoardPagination = ({ basePath, currentPage, totalPages, footerStyle = fals
|
||||
<span className={styles.footerPageBracket}>]</span>
|
||||
</span>
|
||||
)}
|
||||
{pageNumbers.map((page) =>
|
||||
page === currentPage ? (
|
||||
<span key={page} className={styles.footerPageItem}>
|
||||
<span className={styles.footerPageBracket}>[</span>
|
||||
<span className={styles.footerPageCurrent}>{page}</span>
|
||||
<span className={styles.footerPageBracket}>]</span>
|
||||
</span>
|
||||
) : (
|
||||
<span key={page} className={styles.footerPageItem}>
|
||||
<span className={styles.footerPageBracket}>[</span>
|
||||
<Link to={pageHref(page)} className={styles.footerPageLink}>
|
||||
{page}
|
||||
</Link>
|
||||
<span className={styles.footerPageBracket}>]</span>
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
{pageNumbers.map((page) => (
|
||||
<span key={page} className={styles.footerPageItem}>
|
||||
<span className={styles.footerPageBracket}>[</span>
|
||||
<Link to={pageHref(page)} className={page === currentPage ? styles.footerPageCurrent : styles.footerPageLink}>
|
||||
{page}
|
||||
</Link>
|
||||
<span className={styles.footerPageBracket}>]</span>
|
||||
</span>
|
||||
))}
|
||||
{currentPage < totalPages ? (
|
||||
<button type='button' className={styles.pagelistNavButton} onClick={() => navigate(pageHref(currentPage + 1))}>
|
||||
{t('next')}
|
||||
@@ -85,10 +77,10 @@ const BoardPagination = ({ basePath, currentPage, totalPages, footerStyle = fals
|
||||
) : (
|
||||
<span className={styles.footerNavPlainDisabled}>{t('next')}</span>
|
||||
)}
|
||||
<Link to={catalogHref} className={styles.footerPageLink}>
|
||||
<Link to={catalogHref} className={styles.pagelistSeparatorLink}>
|
||||
{t('catalog')}
|
||||
</Link>
|
||||
<Link to={archiveHref} className={styles.footerPageLink}>
|
||||
<Link to={archiveHref} className={styles.pagelistSeparatorLink}>
|
||||
{t('archive')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
width: 180px;
|
||||
position: relative;
|
||||
max-height: 320px;
|
||||
overflow: hidden;
|
||||
padding: 10px 0 3px 0;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
@@ -18,8 +19,6 @@
|
||||
}
|
||||
|
||||
.postContent {
|
||||
overflow: hidden;
|
||||
max-height: 155px;
|
||||
}
|
||||
|
||||
.large {
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 { ContentPreview } from '../../views/home/popular-threads-box';
|
||||
import { removeMarkdown } from '../../lib/utils/post-utils';
|
||||
import PostMenuDesktop from '../post-desktop/post-menu-desktop';
|
||||
import styles from './catalog-row.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
@@ -205,7 +205,7 @@ const CatalogPost = memo(
|
||||
{content ? ': ' : ''}
|
||||
</span>
|
||||
)}
|
||||
{content && <ContentPreview content={content} maxLength={9999} />}
|
||||
{content && removeMarkdown(content)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -482,6 +482,7 @@ const ReplyBacklinks = ({
|
||||
(reply: Comment) =>
|
||||
reply?.parentCid === cid &&
|
||||
reply?.cid &&
|
||||
reply?.number &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={reply.cid} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
{quotedByMap
|
||||
@@ -490,6 +491,7 @@ const ReplyBacklinks = ({
|
||||
(reply: Comment) =>
|
||||
reply?.parentCid !== cid &&
|
||||
reply?.cid &&
|
||||
reply?.number &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={reply.cid} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
</>
|
||||
@@ -502,7 +504,9 @@ const OpBacklinks = ({ cid, quotedByMap }: { cid: string; quotedByMap?: Map<stri
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment) =>
|
||||
reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
reply?.cid &&
|
||||
reply?.number &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -437,7 +437,9 @@ const ReplyBacklinks = ({ post, quotedByMap, directRepliesByParentCid }: ReplyBa
|
||||
?.get(cid)
|
||||
?.map(
|
||||
(reply: Comment) =>
|
||||
reply?.cid && !(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
reply?.cid &&
|
||||
reply?.number &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`op-bl-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
@@ -447,6 +449,7 @@ const ReplyBacklinks = ({ post, quotedByMap, directRepliesByParentCid }: ReplyBa
|
||||
(reply: Comment) =>
|
||||
reply?.parentCid === cid &&
|
||||
reply?.cid &&
|
||||
reply?.number &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={reply.cid} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
{quotedByMap
|
||||
@@ -455,6 +458,7 @@ const ReplyBacklinks = ({ post, quotedByMap, directRepliesByParentCid }: ReplyBa
|
||||
(reply: Comment) =>
|
||||
reply?.parentCid !== cid &&
|
||||
reply?.cid &&
|
||||
reply?.number &&
|
||||
!(reply?.deleted || reply?.removed) && <ReplyQuotePreview key={`qb-${reply.cid}`} isBacklinkReply={true} backlinkReply={reply} />,
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -71,6 +71,15 @@ const scrollToThreadCardTop = (threadCid: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const scrollToReplyOnPage = (cid: string) => {
|
||||
const el = document.querySelector<HTMLElement>(`[data-cid="${cid}"][data-post-cid]`);
|
||||
if (!el) return false;
|
||||
document.querySelectorAll('.scroll-highlight').forEach((prev) => prev.classList.remove('scroll-highlight'));
|
||||
el.scrollIntoView({ behavior: 'auto', block: 'center' });
|
||||
el.classList.add('scroll-highlight');
|
||||
return true;
|
||||
};
|
||||
|
||||
const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, isQuotelinkReply, isOP, showTrailingBreak = true }: ReplyQuotePreviewProps) => {
|
||||
const [hoveredCid, setHoveredCid] = useState<string | null>(null);
|
||||
const [outOfViewCid, setOutOfViewCid] = useState<string | null>(null);
|
||||
@@ -126,6 +135,7 @@ const DesktopQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, i
|
||||
scrollToThreadCardTop(cid);
|
||||
return;
|
||||
}
|
||||
if (scrollToReplyOnPage(cid)) return;
|
||||
navigate(threadRoute);
|
||||
}
|
||||
};
|
||||
@@ -242,6 +252,7 @@ const MobileQuotePreview = ({ backlinkReply, quotelinkReply, isBacklinkReply, is
|
||||
scrollToThreadCardTop(cid);
|
||||
return;
|
||||
}
|
||||
if (scrollToReplyOnPage(cid)) return;
|
||||
navigate(threadRoute);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ vi.mock('react-i18next', () => ({
|
||||
vi.mock('@plebbit/plebbit-react-hooks', () => ({
|
||||
useAccount: () => ({ id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } }),
|
||||
useAccounts: () => ({ accounts: [{ id: 'test-id', name: 'Account 1', author: { shortAddress: '0x1...3' } }] }),
|
||||
createAccount: vi.fn(),
|
||||
deleteAccount: vi.fn(),
|
||||
exportAccount: vi.fn(),
|
||||
importAccount: vi.fn(),
|
||||
@@ -70,13 +69,13 @@ describe('AccountSettings', () => {
|
||||
expect(buttons.some((b) => (b.textContent ?? '').includes('edit'))).toBe(true);
|
||||
});
|
||||
|
||||
it('renders create, import, export buttons', () => {
|
||||
it('renders download_backup and import_account_backup buttons', () => {
|
||||
render(createElement(AccountSettings));
|
||||
const buttons = Array.from(container.querySelectorAll('button'));
|
||||
const texts = buttons.map((b) => b.textContent ?? '');
|
||||
expect(texts.some((t) => t.includes('create'))).toBe(true);
|
||||
expect(texts.some((t) => t.includes('import'))).toBe(true);
|
||||
expect(texts.some((t) => t.includes('export'))).toBe(true);
|
||||
expect(texts.some((t) => t.includes('download_backup'))).toBe(true);
|
||||
expect(texts.some((t) => t.includes('import_account_backup'))).toBe(true);
|
||||
expect(texts.some((t) => t.includes('create'))).toBe(false);
|
||||
});
|
||||
|
||||
it('renders delete_account button', () => {
|
||||
|
||||
@@ -17,11 +17,7 @@
|
||||
color: red;
|
||||
}
|
||||
|
||||
.setting .createAccount {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.warning {
|
||||
.info {
|
||||
font-size: 0.8em;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createAccount, deleteAccount, exportAccount, importAccount, setActiveAccount, useAccount, useAccounts } from '@plebbit/plebbit-react-hooks';
|
||||
import { deleteAccount, exportAccount, importAccount, setActiveAccount, useAccount, useAccounts } from '@plebbit/plebbit-react-hooks';
|
||||
import styles from './account-settings.module.css';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
@@ -33,35 +32,8 @@ const AccountSettingsEditor = ({
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
const { accounts } = useAccounts();
|
||||
const switchToNewAccountRef = useRef(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (switchToNewAccountRef.current && accounts.length > 0) {
|
||||
const lastAccount = accounts[accounts.length - 1];
|
||||
setActiveAccount(lastAccount.name);
|
||||
switchToNewAccountRef.current = false;
|
||||
}
|
||||
}, [accounts]);
|
||||
|
||||
const handleCreateAccount = async () => {
|
||||
const result = await withErrorHandling(
|
||||
async () => {
|
||||
switchToNewAccountRef.current = true;
|
||||
await createAccount();
|
||||
},
|
||||
(error) => {
|
||||
if (error instanceof Error) {
|
||||
alert(error.message);
|
||||
console.log(error);
|
||||
} else {
|
||||
console.error('An unknown error occurred:', error);
|
||||
}
|
||||
},
|
||||
);
|
||||
void result;
|
||||
};
|
||||
|
||||
const _deleteAccount = (accountName: string) => {
|
||||
if (!accountName) {
|
||||
return;
|
||||
@@ -184,29 +156,26 @@ const AccountSettingsEditor = ({
|
||||
|
||||
const accountsOptions = accounts.map((account) => (
|
||||
<option key={account?.id} value={account?.name}>
|
||||
u/{account?.author?.shortAddress}
|
||||
{account?.author?.shortAddress}
|
||||
</option>
|
||||
));
|
||||
|
||||
const host = window.electronApi?.isElectron ? 'this desktop app' : isAndroid ? 'this mobile app' : window.location.hostname;
|
||||
|
||||
return (
|
||||
<div className={styles.setting}>
|
||||
<div>
|
||||
<select value={account?.name} onChange={(e) => setActiveAccount(e.target.value)}>
|
||||
{accountsOptions}
|
||||
</select>
|
||||
<button className={styles.createAccount} onClick={handleCreateAccount}>
|
||||
{t('create')}
|
||||
</button>{' '}
|
||||
<button onClick={handleImportAccount}>{t('import')}</button> <button onClick={handleExportAccount}>{t('export')}</button>
|
||||
<div className={styles.warning}>
|
||||
{t('stored_locally', {
|
||||
location: window.electronApi?.isElectron ? 'this desktop app' : isAndroid ? 'this mobile app' : window.location.hostname,
|
||||
interpolation: { escapeValue: false },
|
||||
})}
|
||||
</select>{' '}
|
||||
<button onClick={() => navigate('/settings/account-data', { state: { returnTo: location.pathname + location.hash } })}>{t('edit')}</button>{' '}
|
||||
<button onClick={handleExportAccount}>{t('download_backup')}</button>
|
||||
<div className={styles.info}>
|
||||
{t('account_auto_generated')} {t('stored_locally', { location: host, interpolation: { escapeValue: false } })}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => navigate('/settings/account-data', { state: { returnTo: location.pathname + location.hash } })}>{t('edit')}</button>
|
||||
<button onClick={handleImportAccount}>{t('import_account_backup')}</button>
|
||||
<button className={styles.deleteAccount} onClick={() => _deleteAccount(account?.name ?? '')}>
|
||||
{t('delete_account')}
|
||||
</button>
|
||||
|
||||
@@ -2,77 +2,92 @@ import { useMemo, useRef } from 'react';
|
||||
import { Comment, Subplebbit } from '@plebbit/plebbit-react-hooks';
|
||||
import { getCommentMediaInfo, getHasThumbnail } from '../lib/utils/media-utils';
|
||||
|
||||
const MAX_POSTS = 8;
|
||||
const MAX_PER_SUB = 3;
|
||||
|
||||
/**
|
||||
* Extracts popular posts from subplebbits.
|
||||
* Uses memoization to avoid recomputing when only updatingState changes.
|
||||
* Ranked by replyCount instead of a static threshold so the box
|
||||
* adapts to both low- and high-activity periods.
|
||||
*
|
||||
* Grow-only commit: once a post enters the grid it never shifts or
|
||||
* disappears — new posts fill remaining slots until the cap is reached.
|
||||
*/
|
||||
const usePopularPosts = (subplebbits: Subplebbit[]) => {
|
||||
// Track the previous CID list to detect actual content changes vs transient state changes
|
||||
const prevCidsRef = useRef<string>('');
|
||||
const committedRef = useRef<{ posts: Comment[]; cids: Set<string> }>({
|
||||
posts: [],
|
||||
cids: new Set(),
|
||||
});
|
||||
const prevInputKeyRef = useRef('');
|
||||
|
||||
// Reset committed when the board set changes (e.g. NSFW filter toggle)
|
||||
const inputKey = subplebbits
|
||||
.map((s) => s?.address)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.join(',');
|
||||
if (prevInputKeyRef.current !== inputKey) {
|
||||
prevInputKeyRef.current = inputKey;
|
||||
committedRef.current = { posts: [], cids: new Set() };
|
||||
}
|
||||
|
||||
const candidates = useMemo(() => {
|
||||
if (committedRef.current.posts.length >= MAX_POSTS) return [];
|
||||
|
||||
const { popularPosts, error } = useMemo(() => {
|
||||
try {
|
||||
const uniqueLinks: Set<string> = new Set();
|
||||
const uniqueLinks = new Set<string>();
|
||||
const allPosts: Comment[] = [];
|
||||
|
||||
// Base quota on boards that currently have loaded hot comments.
|
||||
// Using total directory count can underfill the list when many boards are empty/unavailable.
|
||||
const loadedSubplebbitsCount = subplebbits.filter((sub) => sub?.posts?.pages?.hot?.comments).length;
|
||||
const postsPerSub = [0, 8, 4, 3, 2, 2, 2, 2, 1][Math.min(loadedSubplebbitsCount, 8)];
|
||||
for (const sub of subplebbits) {
|
||||
if (!sub?.posts?.pages?.hot?.comments) continue;
|
||||
|
||||
subplebbits.forEach((subplebbit: any) => {
|
||||
let subplebbitPosts: Comment[] = [];
|
||||
const subPosts: Comment[] = [];
|
||||
for (const post of Object.values(sub.posts.pages.hot.comments as Comment)) {
|
||||
const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, thumbnailUrl } = post;
|
||||
|
||||
if (subplebbit?.posts?.pages?.hot?.comments) {
|
||||
const rawPosts = Object.values(subplebbit.posts.pages.hot.comments as Comment);
|
||||
for (const post of rawPosts) {
|
||||
const { deleted, link, linkHeight, linkWidth, locked, pinned, removed, replyCount, thumbnailUrl } = post;
|
||||
try {
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
try {
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
const hasThumbnail = getHasThumbnail(commentMediaInfo, link);
|
||||
|
||||
if (hasThumbnail && replyCount > 1 && !deleted && !removed && !locked && !pinned && !uniqueLinks.has(link)) {
|
||||
subplebbitPosts.push(post);
|
||||
uniqueLinks.add(link);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error processing post:', err);
|
||||
if (hasThumbnail && !deleted && !removed && !locked && !pinned && !uniqueLinks.has(link)) {
|
||||
subPosts.push(post);
|
||||
uniqueLinks.add(link);
|
||||
}
|
||||
} catch {
|
||||
// skip posts with malformed media URLs
|
||||
}
|
||||
|
||||
subplebbitPosts.sort((a: any, b: any) => b.timestamp - a.timestamp);
|
||||
const selectedPosts = subplebbitPosts.slice(0, postsPerSub);
|
||||
allPosts.push(...selectedPosts);
|
||||
}
|
||||
|
||||
subPosts.sort((a, b) => (b.replyCount ?? 0) - (a.replyCount ?? 0));
|
||||
allPosts.push(...subPosts.slice(0, MAX_PER_SUB));
|
||||
}
|
||||
|
||||
// Primary: most replies first. Tiebreaker: newest first.
|
||||
allPosts.sort((a, b) => {
|
||||
const diff = (b.replyCount ?? 0) - (a.replyCount ?? 0);
|
||||
return diff !== 0 ? diff : (b.timestamp ?? 0) - (a.timestamp ?? 0);
|
||||
});
|
||||
|
||||
const sortedPosts = allPosts.sort((a: any, b: any) => b.timestamp - a.timestamp).slice(0, 8);
|
||||
|
||||
return { popularPosts: sortedPosts, error: null };
|
||||
return allPosts;
|
||||
} catch (err) {
|
||||
console.error('Error in usePopularPosts:', err);
|
||||
return { popularPosts: [], error: 'Failed to fetch popular posts' };
|
||||
return [];
|
||||
}
|
||||
}, [subplebbits]);
|
||||
|
||||
// Create stable reference: only update if the post content actually changes
|
||||
// Build a key from relevant mutable fields, not just CIDs
|
||||
const currentKey = popularPosts.map((p) => `${p.cid}:${p.replyCount}:${p.timestamp}:${p.locked}:${p.pinned}`).join(',');
|
||||
const stablePostsRef = useRef<Comment[]>(popularPosts);
|
||||
const keyChanged = currentKey !== prevCidsRef.current;
|
||||
|
||||
if (keyChanged) {
|
||||
prevCidsRef.current = currentKey;
|
||||
stablePostsRef.current = popularPosts;
|
||||
// Grow-only: committed posts keep their position, new ones fill empty slots
|
||||
const { posts, cids } = committedRef.current;
|
||||
for (const post of candidates) {
|
||||
if (posts.length >= MAX_POSTS) break;
|
||||
if (post.cid && !cids.has(post.cid)) {
|
||||
posts.push(post);
|
||||
cids.add(post.cid);
|
||||
}
|
||||
}
|
||||
|
||||
// Derive loading state from subplebbit states rather than post count
|
||||
// A subplebbit is still loading if it has no posts pages yet and isn't in a terminal state
|
||||
const hasLoadedData = subplebbits.some((sub) => sub?.posts?.pages?.hot?.comments);
|
||||
const isLoading = subplebbits.length > 0 && !hasLoadedData;
|
||||
|
||||
return { popularPosts: stablePostsRef.current, isLoading, error };
|
||||
return { popularPosts: posts, isLoading, error: null as string | null };
|
||||
};
|
||||
|
||||
export default usePopularPosts;
|
||||
|
||||
+4
-2
@@ -82,7 +82,8 @@ hr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
.highlight,
|
||||
.scroll-highlight {
|
||||
background: var(--reply-highlight-background-color) !important;
|
||||
}
|
||||
|
||||
@@ -104,7 +105,8 @@ hr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
.highlight,
|
||||
.scroll-highlight {
|
||||
background: var(--reply-highlight-background-color) !important;
|
||||
border: var(--reply-highlight-border) !important;
|
||||
border-left: var(--reply-highlight-border-left, revert) !important;
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@
|
||||
--hr-height: 0px;
|
||||
|
||||
/* pagelist */
|
||||
--pagelist-background-color: #ead6ca;
|
||||
--pagelist-background-color: #F0E0D6;
|
||||
--pagelist-item-background-color: #f0e0d6;
|
||||
--pagelist-item-border-color: #d9bfb7;
|
||||
--pagelist-item-text-color: #800;
|
||||
@@ -303,7 +303,7 @@
|
||||
--footer-item-background-color-desktop: #eef2ff;
|
||||
|
||||
/* pagelist */
|
||||
--pagelist-background-color: #c9cde8;
|
||||
--pagelist-background-color: #D6DAF0;
|
||||
--pagelist-item-background-color: #d6daf0;
|
||||
--pagelist-item-border-color: #b7c5d9;
|
||||
--pagelist-item-text-color: #89a;
|
||||
|
||||
@@ -8,8 +8,11 @@ import styles from './account-data-editor.module.css';
|
||||
const DEFAULT_RETURN_TO = '/subs/settings#account-settings';
|
||||
|
||||
const loadAce = async () => {
|
||||
const [aceModule] = await Promise.all([import('react-ace'), import('ace-builds/src-noconflict/mode-json'), import('ace-builds/src-noconflict/theme-monokai')]);
|
||||
return aceModule.default;
|
||||
const aceModule = await import('react-ace');
|
||||
await Promise.all([import('ace-builds/src-noconflict/mode-json'), import('ace-builds/src-noconflict/theme-monokai')]);
|
||||
// Vite CJS interop can double-wrap the default export
|
||||
const mod = aceModule.default;
|
||||
return typeof mod === 'function' ? mod : (mod as unknown as { default: typeof mod }).default;
|
||||
};
|
||||
|
||||
const AccountDataEditor = () => {
|
||||
@@ -103,7 +106,7 @@ const AccountDataEditor = () => {
|
||||
</div>
|
||||
<div className={styles.controls}>
|
||||
<button type='button' onClick={handleSave}>
|
||||
{t('save')}
|
||||
{t('save_changes')}
|
||||
</button>
|
||||
<button type='button' onClick={handleReset}>
|
||||
{t('reset_changes')}
|
||||
|
||||
+23
-14
@@ -350,20 +350,29 @@ const Board = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp, i
|
||||
<ErrorDisplay error={subplebbitError} />
|
||||
</div>
|
||||
)}
|
||||
<Virtuoso
|
||||
defaultItemHeight={300}
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={displayFeed.length}
|
||||
data={displayFeed}
|
||||
computeItemKey={(index, post) => post?.cid || `post-${index}`}
|
||||
itemContent={(index, post) => <Post index={index} post={post} />}
|
||||
useWindowScroll={true}
|
||||
components={footerComponents}
|
||||
endReached={effectiveInfiniteScroll && hasMore ? loadMore : undefined}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
{effectiveInfiniteScroll ? (
|
||||
<Virtuoso
|
||||
defaultItemHeight={300}
|
||||
increaseViewportBy={{ bottom: 1200, top: 1200 }}
|
||||
totalCount={displayFeed.length}
|
||||
data={displayFeed}
|
||||
computeItemKey={(index, post) => post?.cid || `post-${index}`}
|
||||
itemContent={(index, post) => <Post index={index} post={post} />}
|
||||
useWindowScroll={true}
|
||||
components={footerComponents}
|
||||
endReached={hasMore ? loadMore : undefined}
|
||||
ref={virtuosoRef}
|
||||
restoreStateFrom={lastVirtuosoState}
|
||||
initialScrollTop={lastVirtuosoState?.scrollTop}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{displayFeed.map((post, index) => (
|
||||
<Post key={post?.cid || `post-${index}`} index={index} post={post} />
|
||||
))}
|
||||
<footerComponents.Footer />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import { useFeedStateString } from '../../hooks/use-state-string';
|
||||
import useWindowWidth from '../../hooks/use-window-width';
|
||||
import useCatalogStyleStore from '../../stores/use-catalog-style-store';
|
||||
import useFeedResetStore from '../../stores/use-feed-reset-store';
|
||||
import useFeedViewSettingsStore from '../../stores/use-feed-view-settings-store';
|
||||
import useSortingStore from '../../stores/use-sorting-store';
|
||||
import useCatalogFiltersStore from '../../stores/use-catalog-filters-store';
|
||||
import { getSubplebbitAddress, isDirectoryBoard, normalizeMultiboardFeedPath } from '../../lib/utils/route-utils';
|
||||
@@ -204,9 +203,9 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
const isInSubscriptionsView = viewType ? viewType === 'subs' : false;
|
||||
const isInModView = viewType ? viewType === 'mod' : false;
|
||||
|
||||
const enableInfiniteScroll = useFeedViewSettingsStore((state) => state.enableInfiniteScroll);
|
||||
const isForcedInfiniteScroll = isInAllView || isInSubscriptionsView || isInModView;
|
||||
const effectiveInfiniteScroll = enableInfiniteScroll || isForcedInfiniteScroll;
|
||||
const isMultiboard = isInAllView || isInSubscriptionsView || isInModView;
|
||||
// Single-board catalogs always cap at maxGuiPages (no infinite scroll beyond the board's page limit)
|
||||
const effectiveInfiniteScroll = isMultiboard;
|
||||
|
||||
const directories = useDirectories();
|
||||
const resolvedAddressFromUrl = useResolvedSubplebbitAddress();
|
||||
@@ -241,7 +240,7 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
const postsPerPage = columnCount <= 2 ? 10 : columnCount === 3 ? 15 : columnCount === 4 ? 20 : 25;
|
||||
|
||||
const community = useDirectoryByAddress(isInAllView || isInSubscriptionsView || isInModView ? undefined : subplebbitAddress);
|
||||
const { guiPostsPerPage: boardPostsPerPage, maxGuiPages, paginationFeedPostsPerPage, infiniteFeedPostsPerPage } = useBoardFeedPageSize(community);
|
||||
const { guiPostsPerPage: boardPostsPerPage, maxGuiPages, paginationFeedPostsPerPage } = useBoardFeedPageSize(community);
|
||||
|
||||
// Canonical redirect for multiboard catalog paths with numeric page segment (e.g. /all/catalog/1w/5 -> /all/catalog/1w)
|
||||
useEffect(() => {
|
||||
@@ -269,35 +268,13 @@ const Catalog = ({ feedCacheKey, viewType, boardIdentifier: boardIdentifierProp,
|
||||
}, [subplebbitAddress]);
|
||||
|
||||
const feedOptions = useMemo(() => {
|
||||
const catalogPostsPerPage =
|
||||
isInAllView || isInSubscriptionsView || isInModView
|
||||
? effectiveInfiniteScroll
|
||||
? 10
|
||||
: 100
|
||||
: effectiveInfiniteScroll
|
||||
? infiniteFeedPostsPerPage
|
||||
: paginationFeedPostsPerPage;
|
||||
|
||||
return {
|
||||
subplebbitAddresses,
|
||||
sortType: feedSortType,
|
||||
postsPerPage: catalogPostsPerPage,
|
||||
postsPerPage: isMultiboard ? 10 : paginationFeedPostsPerPage,
|
||||
filter: createCombinedFilter(filterItems, searchText, subplebbitAddress || 'all', handleFilterMatch),
|
||||
};
|
||||
}, [
|
||||
subplebbitAddresses,
|
||||
feedSortType,
|
||||
isInAllView,
|
||||
isInSubscriptionsView,
|
||||
isInModView,
|
||||
effectiveInfiniteScroll,
|
||||
infiniteFeedPostsPerPage,
|
||||
paginationFeedPostsPerPage,
|
||||
filterItems,
|
||||
searchText,
|
||||
subplebbitAddress,
|
||||
handleFilterMatch,
|
||||
]);
|
||||
}, [subplebbitAddresses, feedSortType, isMultiboard, paginationFeedPostsPerPage, filterItems, searchText, subplebbitAddress, handleFilterMatch]);
|
||||
|
||||
const { feed, hasMore, loadMore, reset } = useFeed(feedOptions);
|
||||
const { accountComments } = useAccountComments();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts';
|
||||
import { useDirectoriesState, useDirectories, DirectoryCommunity } from '../../../hooks/use-directories';
|
||||
import { getBoardPath } from '../../../lib/utils/route-utils';
|
||||
import useDisclaimerModalStore from '../../../stores/use-disclaimer-modal-store';
|
||||
@@ -63,6 +64,15 @@ const BoardsList = ({ multisub }: { multisub: DirectoryCommunity[] }) => {
|
||||
const { useCatalogLinks, boardFilter } = useBoardsFilterStore();
|
||||
const directories = useDirectories();
|
||||
|
||||
const accountSubplebbitAddresses = useAccountsStore(
|
||||
(state) => {
|
||||
const activeAccountId = state.activeAccountId;
|
||||
const activeAccount = activeAccountId ? state.accounts[activeAccountId] : undefined;
|
||||
return Object.keys(activeAccount?.subplebbits || {});
|
||||
},
|
||||
(prev, next) => prev.length === next.length && prev.every((val, idx) => val === next[idx]),
|
||||
);
|
||||
|
||||
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>, address: string) => {
|
||||
e.preventDefault();
|
||||
const boardPath = getBoardPath(address, directories);
|
||||
@@ -595,6 +605,11 @@ const BoardsList = ({ multisub }: { multisub: DirectoryCommunity[] }) => {
|
||||
<li>
|
||||
<Link to='/subs'>Subscriptions</Link>
|
||||
</li>
|
||||
{accountSubplebbitAddresses.length > 0 && (
|
||||
<li>
|
||||
<Link to='/mod'>{t('boards_you_moderate_nav')}</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useHomeFiltersStore from '../../../stores/use-popular-threads-options-store';
|
||||
import styles from '../home.module.css';
|
||||
@@ -11,21 +11,37 @@ const BoxModal = () => {
|
||||
|
||||
const { showWorksafeContentOnly, setShowWorksafeContentOnly, showNsfwContentOnly, setShowNsfwContentOnly } = useHomeFiltersStore();
|
||||
|
||||
const handleClickOutside = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
useEffect(() => {
|
||||
if (!showFilterModal) return;
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (modalRef.current && !modalRef.current.contains(event.target as Node) && buttonRef.current && !buttonRef.current.contains(event.target as Node)) {
|
||||
setShowFilterModal(false);
|
||||
}
|
||||
},
|
||||
[modalRef, buttonRef, setShowFilterModal],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [handleClickOutside]);
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [showFilterModal]);
|
||||
|
||||
const selectFilter = (filter: 'worksafe' | 'nsfw' | 'all') => {
|
||||
if (filter === 'worksafe') {
|
||||
if (showNsfwContentOnly) setShowNsfwContentOnly(false);
|
||||
setShowWorksafeContentOnly(!showWorksafeContentOnly);
|
||||
} else if (filter === 'nsfw') {
|
||||
if (showWorksafeContentOnly) setShowWorksafeContentOnly(false);
|
||||
setShowNsfwContentOnly(!showNsfwContentOnly);
|
||||
} else {
|
||||
setShowWorksafeContentOnly(false);
|
||||
setShowNsfwContentOnly(false);
|
||||
}
|
||||
setShowFilterModal(false);
|
||||
};
|
||||
|
||||
const handleKey = (e: React.KeyboardEvent, filter: 'worksafe' | 'nsfw' | 'all') => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
selectFilter(filter);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -46,70 +62,29 @@ const BoxModal = () => {
|
||||
{showFilterModal && (
|
||||
<div ref={modalRef} className={styles.filterModal}>
|
||||
<div
|
||||
className={`${styles.option} ${showWorksafeContentOnly && styles.selected}`}
|
||||
className={`${styles.option} ${showWorksafeContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (showNsfwContentOnly) {
|
||||
setShowNsfwContentOnly(false);
|
||||
}
|
||||
setShowWorksafeContentOnly(!showWorksafeContentOnly);
|
||||
setShowFilterModal(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (showNsfwContentOnly) {
|
||||
setShowNsfwContentOnly(false);
|
||||
}
|
||||
setShowWorksafeContentOnly(!showWorksafeContentOnly);
|
||||
setShowFilterModal(false);
|
||||
}}
|
||||
onKeyDown={(e) => handleKey(e, 'worksafe')}
|
||||
onClick={() => selectFilter('worksafe')}
|
||||
>
|
||||
{t('show_worksafe_content_only')}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.option} ${showNsfwContentOnly && styles.selected}`}
|
||||
className={`${styles.option} ${showNsfwContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (showWorksafeContentOnly) {
|
||||
setShowWorksafeContentOnly(false);
|
||||
}
|
||||
setShowNsfwContentOnly(!showNsfwContentOnly);
|
||||
setShowFilterModal(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
if (showWorksafeContentOnly) {
|
||||
setShowWorksafeContentOnly(false);
|
||||
}
|
||||
setShowNsfwContentOnly(!showNsfwContentOnly);
|
||||
setShowFilterModal(false);
|
||||
}}
|
||||
onKeyDown={(e) => handleKey(e, 'nsfw')}
|
||||
onClick={() => selectFilter('nsfw')}
|
||||
>
|
||||
{t('show_nsfw_content_only')}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.option} ${!showWorksafeContentOnly && !showNsfwContentOnly && styles.selected}`}
|
||||
className={`${styles.option} ${!showWorksafeContentOnly && !showNsfwContentOnly ? styles.selected : ''}`}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
setShowWorksafeContentOnly(false);
|
||||
setShowNsfwContentOnly(false);
|
||||
setShowFilterModal(false);
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
setShowWorksafeContentOnly(false);
|
||||
setShowNsfwContentOnly(false);
|
||||
setShowFilterModal(false);
|
||||
}}
|
||||
onKeyDown={(e) => handleKey(e, 'all')}
|
||||
onClick={() => selectFilter('all')}
|
||||
>
|
||||
{t('show_all_content')}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,8 @@ import { removeMarkdown } from '../../../lib/utils/post-utils';
|
||||
|
||||
interface PopularThreadProps {
|
||||
post: Comment;
|
||||
directories: DirectoryCommunity[];
|
||||
boardTitle: string;
|
||||
boardPath: string;
|
||||
}
|
||||
|
||||
export const ContentPreview = ({ content, maxLength = 99 }: { content: string; maxLength?: number }) => {
|
||||
@@ -25,23 +26,17 @@ export const ContentPreview = ({ content, maxLength = 99 }: { content: string; m
|
||||
return truncatedText;
|
||||
};
|
||||
|
||||
// Memoize to prevent rerenders when parent rerenders due to updatingState
|
||||
const PopularThreadCard = memo(
|
||||
({ post, directories }: PopularThreadProps) => {
|
||||
const { cid, content, link, linkHeight, linkWidth, subplebbitAddress, thumbnailUrl, title } = post || {};
|
||||
({ post, boardTitle, boardPath }: PopularThreadProps) => {
|
||||
const { cid, content, link, linkHeight, linkWidth, thumbnailUrl, title } = post || {};
|
||||
const commentMediaInfo = getCommentMediaInfo(link, thumbnailUrl, linkWidth, linkHeight);
|
||||
|
||||
// Find the matching DirectoryCommunity entry and get its title
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === subplebbitAddress);
|
||||
const boardTitle = directoriesEntry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
||||
const boardPath = subplebbitAddress ? getBoardPath(subplebbitAddress, directories) : '';
|
||||
|
||||
return (
|
||||
<div className={styles.popularThread} key={cid}>
|
||||
<div className={styles.title}>{boardTitle}</div>
|
||||
<div className={styles.mediaContainer}>
|
||||
<Link to={`/${boardPath}/thread/${cid}`}>
|
||||
<CatalogPostMedia commentMediaInfo={commentMediaInfo} isOutOfFeed={true} cid={cid} />
|
||||
<CatalogPostMedia commentMediaInfo={commentMediaInfo} isOutOfFeed={true} cid={cid} linkWidth={linkWidth} linkHeight={linkHeight} />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={styles.threadContent}>
|
||||
@@ -56,35 +51,31 @@ const PopularThreadCard = memo(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
// Custom equality: rerender if post.cid or directories entry title changes
|
||||
(prevProps, nextProps) => {
|
||||
if (prevProps.post?.cid !== nextProps.post?.cid) return false;
|
||||
// Compare the relevant directories entry for this post's subplebbitAddress
|
||||
const prevEntry = prevProps.directories.find((ms) => ms?.address === prevProps.post?.subplebbitAddress);
|
||||
const nextEntry = nextProps.directories.find((ms) => ms?.address === nextProps.post?.subplebbitAddress);
|
||||
return prevEntry?.title === nextEntry?.title;
|
||||
},
|
||||
(prevProps, nextProps) => prevProps.post?.cid === nextProps.post?.cid && prevProps.boardTitle === nextProps.boardTitle,
|
||||
);
|
||||
|
||||
const PopularThreadsBox = ({ directories, subplebbits }: { directories: DirectoryCommunity[]; subplebbits: any }) => {
|
||||
const { t } = useTranslation();
|
||||
const { showWorksafeContentOnly, showNsfwContentOnly } = usePopularThreadsOptionsStore();
|
||||
|
||||
const directoryByAddress = useMemo(() => new Map(directories.map((d) => [d.address, d])), [directories]);
|
||||
|
||||
const filteredSubplebbits = useMemo(() => {
|
||||
if (showWorksafeContentOnly) {
|
||||
return subplebbits.filter((sub: Subplebbit) => {
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
|
||||
return directoriesEntry ? !directoriesEntry.nsfw : true;
|
||||
const entry = directoryByAddress.get(sub?.address);
|
||||
return entry ? !entry.nsfw : true;
|
||||
});
|
||||
}
|
||||
if (showNsfwContentOnly) {
|
||||
return subplebbits.filter((sub: Subplebbit) => {
|
||||
const directoriesEntry = directories.find((ms) => ms?.address === sub?.address);
|
||||
return directoriesEntry ? directoriesEntry.nsfw : false;
|
||||
const entry = directoryByAddress.get(sub?.address);
|
||||
return entry ? entry.nsfw : false;
|
||||
});
|
||||
}
|
||||
return subplebbits;
|
||||
}, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directories]);
|
||||
}, [subplebbits, showWorksafeContentOnly, showNsfwContentOnly, directoryByAddress]);
|
||||
|
||||
const { popularPosts } = usePopularPosts(filteredSubplebbits);
|
||||
const isLoading = popularPosts.length === 0;
|
||||
|
||||
@@ -98,7 +89,12 @@ const PopularThreadsBox = ({ directories, subplebbits }: { directories: Director
|
||||
{isLoading ? (
|
||||
<LoadingEllipsis string={t('loading')} />
|
||||
) : (
|
||||
popularPosts.map((post: any) => <PopularThreadCard key={post.cid} post={post} directories={directories} />)
|
||||
popularPosts.map((post: Comment) => {
|
||||
const entry = directoryByAddress.get(post.subplebbitAddress);
|
||||
const boardTitle = entry?.title?.replace(/^\/[^/]+\/\s*-\s*/, '') || '';
|
||||
const boardPath = post.subplebbitAddress ? getBoardPath(post.subplebbitAddress, directories) : '';
|
||||
return <PopularThreadCard key={post.cid} post={post} boardTitle={boardTitle} boardPath={boardPath} />;
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4581,6 +4581,78 @@
|
||||
viem "2.45.0"
|
||||
zod "4.3.6"
|
||||
|
||||
"@plebbit/plebbit-js@https://github.com/plebbit/plebbit-js.git#fd7486d31ce71b29b6dc49c019278af8fb0b0d45":
|
||||
version "0.0.11"
|
||||
resolved "https://github.com/plebbit/plebbit-js.git#fd7486d31ce71b29b6dc49c019278af8fb0b0d45"
|
||||
dependencies:
|
||||
"@bonfida/spl-name-service" "3.0.16"
|
||||
"@enhances/with-resolvers" "0.0.5"
|
||||
"@helia/block-brokers" "5.0.13"
|
||||
"@helia/delegated-routing-v1-http-api-client" "5.1.4"
|
||||
"@helia/ipns" "9.1.5"
|
||||
"@helia/unixfs" "6.0.4"
|
||||
"@libp2p/crypto" "5.1.13"
|
||||
"@libp2p/fetch" "4.0.9"
|
||||
"@libp2p/gossipsub" "15.0.11"
|
||||
"@libp2p/identify" "4.0.9"
|
||||
"@libp2p/interfaces" "3.3.2"
|
||||
"@libp2p/peer-id" "6.0.4"
|
||||
"@libp2p/peer-id-factory" "4.2.4"
|
||||
"@libp2p/websockets" "10.1.2"
|
||||
"@mintpass/challenge" "1.1.11"
|
||||
"@plebbit/plebbit-logger" "git+https://github.com/plebbit/plebbit-logger.git#355a96d7659ed820047980049dfa627d30d83a69"
|
||||
"@plebbit/proper-lockfile" "git+https://github.com/plebbit/node-proper-lockfile.git#7fd6332117340c1d3d98dd0afee2d31cc06f72b8"
|
||||
"@types/proper-lockfile" "4.1.2"
|
||||
assert "2.1.0"
|
||||
better-sqlite3 "12.4.1"
|
||||
blockstore-core "6.1.1"
|
||||
blockstore-fs "3.0.2"
|
||||
blockstore-idb "3.0.1"
|
||||
buffer "6.0.3"
|
||||
captcha-canvas "3.3.4"
|
||||
cbor "9.0.1"
|
||||
debounce "1.2.1"
|
||||
err-code "3.0.1"
|
||||
ethers "6.16.0"
|
||||
ext-name "5.0.0"
|
||||
helia "6.0.14"
|
||||
hpagent "1.2.0"
|
||||
it-all "3.0.6"
|
||||
jose "4.11.0"
|
||||
js-sha256 "0.11.1"
|
||||
js-sha512 "0.9.0"
|
||||
kubo-rpc-client "6.0.2"
|
||||
limiter-es6-compat "2.1.2"
|
||||
localforage "1.10.0"
|
||||
lodash.merge "4.6.2"
|
||||
lru-cache "10.1.0"
|
||||
open-graph-scraper "6.10.0"
|
||||
p-limit "4.0.0"
|
||||
p-timeout "6.1.2"
|
||||
peer-id "0.16.0"
|
||||
probe-image-size "7.2.3"
|
||||
remeda "1.57.0"
|
||||
retry "0.13.1"
|
||||
rpc-websockets "9.3.1"
|
||||
safe-stable-stringify "2.4.3"
|
||||
sha1-uint8array "0.10.7"
|
||||
skia-canvas "3.0.8"
|
||||
tcp-port-used "1.0.2"
|
||||
tiny-typed-emitter "2.1.0"
|
||||
tinycache "1.1.2"
|
||||
ts-custom-error "3.3.1"
|
||||
typestub-ipfs-only-hash "4.0.0"
|
||||
uuid "11.1.0"
|
||||
viem "2.45.0"
|
||||
zod "4.3.6"
|
||||
|
||||
"@plebbit/plebbit-logger@git+https://github.com/plebbit/plebbit-logger.git#355a96d7659ed820047980049dfa627d30d83a69":
|
||||
version "0.0.1"
|
||||
uid "355a96d7659ed820047980049dfa627d30d83a69"
|
||||
resolved "git+https://github.com/plebbit/plebbit-logger.git#355a96d7659ed820047980049dfa627d30d83a69"
|
||||
dependencies:
|
||||
debug "4.3.4"
|
||||
|
||||
"@plebbit/plebbit-logger@github:plebbit/plebbit-logger#355a96d7659ed820047980049dfa627d30d83a69":
|
||||
version "0.0.1"
|
||||
uid "355a96d7659ed820047980049dfa627d30d83a69"
|
||||
@@ -4610,6 +4682,30 @@
|
||||
uuid "11.1.0"
|
||||
zustand "4.0.0"
|
||||
|
||||
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#a828bd598bcc392ed379136feaee0a7b1c8cd23f":
|
||||
version "0.0.1"
|
||||
resolved "https://github.com/plebbit/plebbit-react-hooks.git#a828bd598bcc392ed379136feaee0a7b1c8cd23f"
|
||||
dependencies:
|
||||
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#fd7486d31ce71b29b6dc49c019278af8fb0b0d45"
|
||||
"@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git"
|
||||
assert "2.0.0"
|
||||
ethers "5.6.9"
|
||||
localforage "1.10.0"
|
||||
lodash.isequal "4.5.0"
|
||||
memoizee "0.4.15"
|
||||
quick-lru "5.1.1"
|
||||
uint8arrays "3.1.1"
|
||||
uuid "11.1.0"
|
||||
zustand "4.0.0"
|
||||
|
||||
"@plebbit/proper-lockfile@git+https://github.com/plebbit/node-proper-lockfile.git#7fd6332117340c1d3d98dd0afee2d31cc06f72b8":
|
||||
version "4.1.2"
|
||||
resolved "git+https://github.com/plebbit/node-proper-lockfile.git#7fd6332117340c1d3d98dd0afee2d31cc06f72b8"
|
||||
dependencies:
|
||||
graceful-fs "^4.2.4"
|
||||
retry "^0.12.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
"@plebbit/proper-lockfile@github:plebbit/node-proper-lockfile#7fd6332117340c1d3d98dd0afee2d31cc06f72b8":
|
||||
version "4.1.2"
|
||||
resolved "https://codeload.github.com/plebbit/node-proper-lockfile/tar.gz/7fd6332117340c1d3d98dd0afee2d31cc06f72b8"
|
||||
|
||||
Reference in New Issue
Block a user