Review settings upgrades before applying defaults

Adds a review modal for settings upgrades, keeps the settings banner reviewable, and preserves explicit router settings during account import.
This commit is contained in:
Tommaso Casaburi
2026-06-26 17:14:31 +07:00
committed by GitHub
parent 802aa2a447
commit c6d048ecd7
56 changed files with 1588 additions and 63 deletions
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "وضع متصفح P2P خالص", "advanced_pure_p2p_browser_aria": "وضع متصفح P2P خالص",
"advanced_options_saved_reloading": "تم حفظ الخيارات، جارٍ إعادة التحميل...", "advanced_options_saved_reloading": "تم حفظ الخيارات، جارٍ إعادة التحميل...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "خطأ في حفظ الخيارات: {{message}}" "advanced_error_saving_options": "خطأ في حفظ الخيارات: {{message}}",
"advanced_settings_upgrade_available": "ترقية الإعدادات متاحة",
"advanced_settings_upgrade_tip": "هذه إعدادات 5chan الافتراضية الجديدة المفقودة من هذا الحساب. قم بإلغاء تحديد أي شيء لا تريده.",
"advanced_settings_upgrade_http_routers": "إضافة موجهات HTTP جديدة",
"advanced_settings_upgrade_http_router_aria": "إضافة {{router}} إلى موجهات HTTP",
"settings_upgrade_keep_current": "الإبقاء على الإعدادات الحالية",
"settings_upgrade_apply_selected": "تطبيق التغييرات المحددة",
"settings_upgrade_error_saving": "تعذر حفظ ترقية الإعدادات.",
"settings_upgrade_review_notice": "ترقية الإعدادات متاحة",
"settings_upgrade_review_button": "مراجعة",
"settings_upgrade_never_show_again": "لا تظهر مرة أخرى"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "বিশুদ্ধ P2P ব্রাউজার মোড", "advanced_pure_p2p_browser_aria": "বিশুদ্ধ P2P ব্রাউজার মোড",
"advanced_options_saved_reloading": "বিকল্পগুলো সংরক্ষণ করা হয়েছে, পুনরায় লোড করা হচ্ছে...", "advanced_options_saved_reloading": "বিকল্পগুলো সংরক্ষণ করা হয়েছে, পুনরায় লোড করা হচ্ছে...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "বিকল্প সংরক্ষণে ত্রুটি: {{message}}" "advanced_error_saving_options": "বিকল্প সংরক্ষণে ত্রুটি: {{message}}",
"advanced_settings_upgrade_available": "সেটিংস আপগ্রেড উপলব্ধ",
"advanced_settings_upgrade_tip": "এগুলো এই অ্যাকাউন্টে অনুপস্থিত নতুন 5chan ডিফল্ট। যা চান না, তা আনচেক করুন।",
"advanced_settings_upgrade_http_routers": "নতুন HTTP রাউটার যোগ করুন",
"advanced_settings_upgrade_http_router_aria": "HTTP রাউটারে {{router}} যোগ করুন",
"settings_upgrade_keep_current": "বর্তমান সেটিংস রাখুন",
"settings_upgrade_apply_selected": "নির্বাচিত পরিবর্তনগুলি প্রয়োগ করুন",
"settings_upgrade_error_saving": "সেটিংস আপগ্রেড সংরক্ষণ করা যায়নি।",
"settings_upgrade_review_notice": "সেটিংস আপগ্রেড উপলব্ধ",
"settings_upgrade_review_button": "পর্যালোচনা",
"settings_upgrade_never_show_again": "আর দেখাবেন না"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Režim čistého P2P prohlížeče", "advanced_pure_p2p_browser_aria": "Režim čistého P2P prohlížeče",
"advanced_options_saved_reloading": "Možnosti uloženy, načítání...", "advanced_options_saved_reloading": "Možnosti uloženy, načítání...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Chyba při ukládání možností: {{message}}" "advanced_error_saving_options": "Chyba při ukládání možností: {{message}}",
"advanced_settings_upgrade_available": "upgrade nastavení k dispozici",
"advanced_settings_upgrade_tip": "Toto jsou nová výchozí nastavení 5chan, která v tomto účtu chybí. Zrušte zaškrtnutí u všeho, co nechcete.",
"advanced_settings_upgrade_http_routers": "přidat nové HTTP směrovače",
"advanced_settings_upgrade_http_router_aria": "Přidat {{router}} do HTTP směrovačů",
"settings_upgrade_keep_current": "ponechat stávající nastavení",
"settings_upgrade_apply_selected": "použít vybrané změny",
"settings_upgrade_error_saving": "Nepodařilo se uložit upgrade nastavení.",
"settings_upgrade_review_notice": "upgrade nastavení k dispozici",
"settings_upgrade_review_button": "zkontrolovat",
"settings_upgrade_never_show_again": "už nezobrazovat"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Ren P2P-browsertilstand", "advanced_pure_p2p_browser_aria": "Ren P2P-browsertilstand",
"advanced_options_saved_reloading": "Indstillinger gemt, genindlæser...", "advanced_options_saved_reloading": "Indstillinger gemt, genindlæser...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Fejl ved lagring af indstillinger: {{message}}" "advanced_error_saving_options": "Fejl ved lagring af indstillinger: {{message}}",
"advanced_settings_upgrade_available": "indstillingsopgradering tilgængelig",
"advanced_settings_upgrade_tip": "Dette er nye 5chan-standarder, der mangler på denne konto. Fjern markeringen for alt, du ikke ønsker.",
"advanced_settings_upgrade_http_routers": "tilføj nye HTTP-routere",
"advanced_settings_upgrade_http_router_aria": "Tilføj {{router}} til HTTP-routere",
"settings_upgrade_keep_current": "behold nuværende indstillinger",
"settings_upgrade_apply_selected": "anvend valgte ændringer",
"settings_upgrade_error_saving": "Kunne ikke gemme indstillingsopgraderingen.",
"settings_upgrade_review_notice": "indstillingsopgradering tilgængelig",
"settings_upgrade_review_button": "gennemgå",
"settings_upgrade_never_show_again": "vis aldrig igen"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Reiner P2P-Browsermodus", "advanced_pure_p2p_browser_aria": "Reiner P2P-Browsermodus",
"advanced_options_saved_reloading": "Optionen gespeichert, wird neu geladen...", "advanced_options_saved_reloading": "Optionen gespeichert, wird neu geladen...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Fehler beim Speichern der Optionen: {{message}}" "advanced_error_saving_options": "Fehler beim Speichern der Optionen: {{message}}",
"advanced_settings_upgrade_available": "Einstellungs-Upgrade verfügbar",
"advanced_settings_upgrade_tip": "Dies sind neue 5chan-Standardeinstellungen, die in diesem Konto fehlen. Deaktivieren Sie alles, was Sie nicht möchten.",
"advanced_settings_upgrade_http_routers": "neue HTTP-Router hinzufügen",
"advanced_settings_upgrade_http_router_aria": "{{router}} zu HTTP-Routern hinzufügen",
"settings_upgrade_keep_current": "aktuelle Einstellungen beibehalten",
"settings_upgrade_apply_selected": "ausgewählte Änderungen anwenden",
"settings_upgrade_error_saving": "Die Einstellungsaktualisierung konnte nicht gespeichert werden.",
"settings_upgrade_review_notice": "Einstellungs-Upgrade verfügbar",
"settings_upgrade_review_button": "überprüfen",
"settings_upgrade_never_show_again": "nie wieder anzeigen"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Λειτουργία καθαρού P2P προγράμματος περιήγησης", "advanced_pure_p2p_browser_aria": "Λειτουργία καθαρού P2P προγράμματος περιήγησης",
"advanced_options_saved_reloading": "Οι επιλογές αποθηκεύτηκαν, επαναφόρτωση...", "advanced_options_saved_reloading": "Οι επιλογές αποθηκεύτηκαν, επαναφόρτωση...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Σφάλμα αποθήκευσης ρυθμίσεων: {{message}}" "advanced_error_saving_options": "Σφάλμα αποθήκευσης ρυθμίσεων: {{message}}",
"advanced_settings_upgrade_available": "αναβάθμιση ρυθμίσεων διαθέσιμη",
"advanced_settings_upgrade_tip": "Αυτές είναι νέες προεπιλογές 5chan που λείπουν από αυτόν τον λογαριασμό. Αποεπιλέξτε ό,τι δεν θέλετε.",
"advanced_settings_upgrade_http_routers": "προσθήκη νέων δρομολογητών HTTP",
"advanced_settings_upgrade_http_router_aria": "Προσθήκη {{router}} σε δρομολογητές HTTP",
"settings_upgrade_keep_current": "διατήρηση τρεχουσών ρυθμίσεων",
"settings_upgrade_apply_selected": "εφαρμογή επιλεγμένων αλλαγών",
"settings_upgrade_error_saving": "Δεν ήταν δυνατή η αποθήκευση της αναβάθμισης ρυθμίσεων.",
"settings_upgrade_review_notice": "αναβάθμιση ρυθμίσεων διαθέσιμη",
"settings_upgrade_review_button": "έλεγχος",
"settings_upgrade_never_show_again": "να μην εμφανιστεί ξανά"
} }
+11 -1
View File
@@ -451,5 +451,15 @@
"tex_preview_title": "<tex>TeX</tex> Preview", "tex_preview_title": "<tex>TeX</tex> Preview",
"tex_preview_protip": "Use [math][/math] tags for inline, and [eqn][/eqn] tags for block equations.", "tex_preview_protip": "Use [math][/math] tags for inline, and [eqn][/eqn] tags for block equations.",
"tex_preview_input_label": "TeX source", "tex_preview_input_label": "TeX source",
"preview_tex_equations": "Preview TeX equations" "preview_tex_equations": "Preview TeX equations",
"advanced_settings_upgrade_available": "settings upgrade available",
"advanced_settings_upgrade_tip": "These are new 5chan defaults missing from this account. Uncheck anything you do not want.",
"advanced_settings_upgrade_http_routers": "add new HTTP routers",
"advanced_settings_upgrade_http_router_aria": "Add {{router}} to HTTP routers",
"settings_upgrade_keep_current": "keep current settings",
"settings_upgrade_apply_selected": "apply selected changes",
"settings_upgrade_error_saving": "Could not save the settings upgrade.",
"settings_upgrade_review_notice": "settings upgrade available",
"settings_upgrade_review_button": "review",
"settings_upgrade_never_show_again": "never show again"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Modo de navegador P2P puro", "advanced_pure_p2p_browser_aria": "Modo de navegador P2P puro",
"advanced_options_saved_reloading": "Opciones guardadas, recargando...", "advanced_options_saved_reloading": "Opciones guardadas, recargando...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Error al guardar las opciones: {{message}}" "advanced_error_saving_options": "Error al guardar las opciones: {{message}}",
"advanced_settings_upgrade_available": "actualización de configuración disponible",
"advanced_settings_upgrade_tip": "Estos son nuevos valores predeterminados de 5chan que faltan en esta cuenta. Desmarca lo que no quieras.",
"advanced_settings_upgrade_http_routers": "añadir nuevos enrutadores HTTP",
"advanced_settings_upgrade_http_router_aria": "Agregar {{router}} a los enrutadores HTTP",
"settings_upgrade_keep_current": "mantener la configuración actual",
"settings_upgrade_apply_selected": "aplicar cambios seleccionados",
"settings_upgrade_error_saving": "No se pudo guardar la actualización de configuración.",
"settings_upgrade_review_notice": "actualización de configuración disponible",
"settings_upgrade_review_button": "revisar",
"settings_upgrade_never_show_again": "no volver a mostrar"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "حالت مرورگر P2P خالص", "advanced_pure_p2p_browser_aria": "حالت مرورگر P2P خالص",
"advanced_options_saved_reloading": "گزینه‌ها ذخیره شد، در حال بارگذاری مجدد...", "advanced_options_saved_reloading": "گزینه‌ها ذخیره شد، در حال بارگذاری مجدد...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "خطا در ذخیره گزینه‌ها: {{message}}" "advanced_error_saving_options": "خطا در ذخیره گزینه‌ها: {{message}}",
"advanced_settings_upgrade_available": "ارتقای تنظیمات موجود است",
"advanced_settings_upgrade_tip": "این‌ها تنظیمات پیش‌فرض جدید 5chan هستند که در این حساب وجود ندارند. هر موردی را که نمی‌خواهید، از حالت انتخاب خارج کنید.",
"advanced_settings_upgrade_http_routers": "افزودن مسیریاب‌های HTTP جدید",
"advanced_settings_upgrade_http_router_aria": "افزودن {{router}} به مسیریاب‌های HTTP",
"settings_upgrade_keep_current": "حفظ تنظیمات فعلی",
"settings_upgrade_apply_selected": "اعمال تغییرات انتخاب‌شده",
"settings_upgrade_error_saving": "امکان ذخیرهٔ ارتقای تنظیمات وجود نداشت.",
"settings_upgrade_review_notice": "ارتقای تنظیمات موجود است",
"settings_upgrade_review_button": "بررسی",
"settings_upgrade_never_show_again": "دیگر نشان نده"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Puhdas P2P-selaimen tila", "advanced_pure_p2p_browser_aria": "Puhdas P2P-selaimen tila",
"advanced_options_saved_reloading": "Asetukset tallennettu, ladataan uudelleen...", "advanced_options_saved_reloading": "Asetukset tallennettu, ladataan uudelleen...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Virhe asetusten tallennuksessa: {{message}}" "advanced_error_saving_options": "Virhe asetusten tallennuksessa: {{message}}",
"advanced_settings_upgrade_available": "asetuspäivitys saatavilla",
"advanced_settings_upgrade_tip": "Nämä ovat uusia 5chan-oletusasetuksia, jotka puuttuvat tältä tililtä. Poista valinta kaikista, joita et halua.",
"advanced_settings_upgrade_http_routers": "lisää uudet HTTP-reitittimet",
"advanced_settings_upgrade_http_router_aria": "Lisää {{router}} HTTP-reitittimiin",
"settings_upgrade_keep_current": "säilytä nykyiset asetukset",
"settings_upgrade_apply_selected": "käytä valitut muutokset",
"settings_upgrade_error_saving": "Asetusten päivitystä ei voitu tallentaa.",
"settings_upgrade_review_notice": "asetuspäivitys saatavilla",
"settings_upgrade_review_button": "tarkista",
"settings_upgrade_never_show_again": "älä näytä uudelleen"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Purong P2P browser mode", "advanced_pure_p2p_browser_aria": "Purong P2P browser mode",
"advanced_options_saved_reloading": "Na-save ang mga opsyon, nagre-reload...", "advanced_options_saved_reloading": "Na-save ang mga opsyon, nagre-reload...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Error sa pag-save ng mga opsyon: {{message}}" "advanced_error_saving_options": "Error sa pag-save ng mga opsyon: {{message}}",
"advanced_settings_upgrade_available": "may upgrade ng setting",
"advanced_settings_upgrade_tip": "Ito ang mga bagong default ng 5chan na wala sa account na ito. Alisin ang check sa anumang ayaw mo.",
"advanced_settings_upgrade_http_routers": "magdagdag ng mga bagong HTTP routers",
"advanced_settings_upgrade_http_router_aria": "Idagdag ang {{router}} sa HTTP routers",
"settings_upgrade_keep_current": "panatilihin ang kasalukuyang mga setting",
"settings_upgrade_apply_selected": "ilapat ang mga napiling pagbabago",
"settings_upgrade_error_saving": "Hindi ma-save ang upgrade ng mga setting.",
"settings_upgrade_review_notice": "may upgrade ng setting",
"settings_upgrade_review_button": "suriin",
"settings_upgrade_never_show_again": "huwag nang ipakita muli"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Mode navigateur P2P pur", "advanced_pure_p2p_browser_aria": "Mode navigateur P2P pur",
"advanced_options_saved_reloading": "Options enregistrées, rechargement...", "advanced_options_saved_reloading": "Options enregistrées, rechargement...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Erreur lors de l'enregistrement des options : {{message}}" "advanced_error_saving_options": "Erreur lors de l'enregistrement des options : {{message}}",
"advanced_settings_upgrade_available": "mise à jour des paramètres disponible",
"advanced_settings_upgrade_tip": "Ce sont de nouvelles valeurs par défaut 5chan absentes de ce compte. Décochez tout ce que vous ne voulez pas.",
"advanced_settings_upgrade_http_routers": "ajouter de nouveaux routeurs HTTP",
"advanced_settings_upgrade_http_router_aria": "Ajouter {{router}} aux routeurs HTTP",
"settings_upgrade_keep_current": "conserver les paramètres actuels",
"settings_upgrade_apply_selected": "appliquer les modifications sélectionnées",
"settings_upgrade_error_saving": "Impossible d'enregistrer la mise à niveau des paramètres.",
"settings_upgrade_review_notice": "mise à jour des paramètres disponible",
"settings_upgrade_review_button": "examiner",
"settings_upgrade_never_show_again": "ne plus afficher"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "מצב דפדפן P2P טהור", "advanced_pure_p2p_browser_aria": "מצב דפדפן P2P טהור",
"advanced_options_saved_reloading": "ההגדרות נשמרו, טוען מחדש...", "advanced_options_saved_reloading": "ההגדרות נשמרו, טוען מחדש...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "שגיאה בשמירת האפשרויות: {{message}}" "advanced_error_saving_options": "שגיאה בשמירת האפשרויות: {{message}}",
"advanced_settings_upgrade_available": "שדרוג הגדרות זמין",
"advanced_settings_upgrade_tip": "אלה ברירות המחדל החדשות של 5chan שחסרות בחשבון זה. בטל סימון של כל מה שאינך רוצה.",
"advanced_settings_upgrade_http_routers": "הוסף נתבי HTTP חדשים",
"advanced_settings_upgrade_http_router_aria": "הוסף את {{router}} לנתבי HTTP",
"settings_upgrade_keep_current": "שמור על ההגדרות הנוכחיות",
"settings_upgrade_apply_selected": "החל שינויים שנבחרו",
"settings_upgrade_error_saving": "לא ניתן לשמור את שדרוג ההגדרות.",
"settings_upgrade_review_notice": "שדרוג הגדרות זמין",
"settings_upgrade_review_button": "סקור",
"settings_upgrade_never_show_again": "לא להציג שוב"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "शुद्ध P2P ब्राउज़र मोड", "advanced_pure_p2p_browser_aria": "शुद्ध P2P ब्राउज़र मोड",
"advanced_options_saved_reloading": "विकल्प सहेजे गए, पुनः लोड हो रहा है...", "advanced_options_saved_reloading": "विकल्प सहेजे गए, पुनः लोड हो रहा है...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "विकल्प सहेजने में त्रुटि: {{message}}" "advanced_error_saving_options": "विकल्प सहेजने में त्रुटि: {{message}}",
"advanced_settings_upgrade_available": "सेटिंग्स अपग्रेड उपलब्ध",
"advanced_settings_upgrade_tip": "ये नए 5chan डिफ़ॉल्ट हैं जो इस खाते में नहीं हैं। जो आप नहीं चाहते, उसे अनचेक करें।",
"advanced_settings_upgrade_http_routers": "नए HTTP राउटर जोड़ें",
"advanced_settings_upgrade_http_router_aria": "HTTP राउटर में {{router}} जोड़ें",
"settings_upgrade_keep_current": "वर्तमान सेटिंग्स रखें",
"settings_upgrade_apply_selected": "चयनित परिवर्तन लागू करें",
"settings_upgrade_error_saving": "सेटिंग्स अपग्रेड सहेज नहीं सके।",
"settings_upgrade_review_notice": "सेटिंग्स अपग्रेड उपलब्ध",
"settings_upgrade_review_button": "समीक्षा करें",
"settings_upgrade_never_show_again": "फिर कभी न दिखाएँ"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Tiszta P2P böngésző mód", "advanced_pure_p2p_browser_aria": "Tiszta P2P böngésző mód",
"advanced_options_saved_reloading": "Beállítások mentve, újratöltés...", "advanced_options_saved_reloading": "Beállítások mentve, újratöltés...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Hiba a beállítások mentésekor: {{message}}" "advanced_error_saving_options": "Hiba a beállítások mentésekor: {{message}}",
"advanced_settings_upgrade_available": "beállításfrissítés elérhető",
"advanced_settings_upgrade_tip": "Ezek olyan új 5chan-alapértelmezések, amelyek hiányoznak ebből a fiókból. Vegye ki a pipát mindenből, amit nem szeretne.",
"advanced_settings_upgrade_http_routers": "új HTTP routerek hozzáadása",
"advanced_settings_upgrade_http_router_aria": "{{router}} hozzáadása HTTP routerekhez",
"settings_upgrade_keep_current": "jelenlegi beállítások megtartása",
"settings_upgrade_apply_selected": "kiválasztott módosítások alkalmazása",
"settings_upgrade_error_saving": "A beállítások frissítése nem menthető el.",
"settings_upgrade_review_notice": "beállításfrissítés elérhető",
"settings_upgrade_review_button": "áttekintés",
"settings_upgrade_never_show_again": "ne jelenjen meg újra"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Mode browser P2P murni", "advanced_pure_p2p_browser_aria": "Mode browser P2P murni",
"advanced_options_saved_reloading": "Opsi disimpan, memuat ulang...", "advanced_options_saved_reloading": "Opsi disimpan, memuat ulang...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Kesalahan saat menyimpan opsi: {{message}}" "advanced_error_saving_options": "Kesalahan saat menyimpan opsi: {{message}}",
"advanced_settings_upgrade_available": "pembaruan pengaturan tersedia",
"advanced_settings_upgrade_tip": "Ini adalah default 5chan baru yang belum ada di akun ini. Hapus centang pada apa pun yang tidak Anda inginkan.",
"advanced_settings_upgrade_http_routers": "tambahkan router HTTP baru",
"advanced_settings_upgrade_http_router_aria": "Tambahkan {{router}} ke router HTTP",
"settings_upgrade_keep_current": "pertahankan pengaturan saat ini",
"settings_upgrade_apply_selected": "terapkan perubahan yang dipilih",
"settings_upgrade_error_saving": "Pembaruan pengaturan tidak dapat disimpan.",
"settings_upgrade_review_notice": "pembaruan pengaturan tersedia",
"settings_upgrade_review_button": "tinjau",
"settings_upgrade_never_show_again": "jangan tampilkan lagi"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Modalità browser P2P puro", "advanced_pure_p2p_browser_aria": "Modalità browser P2P puro",
"advanced_options_saved_reloading": "Opzioni salvate, ricaricamento...", "advanced_options_saved_reloading": "Opzioni salvate, ricaricamento...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Errore nel salvataggio delle opzioni: {{message}}" "advanced_error_saving_options": "Errore nel salvataggio delle opzioni: {{message}}",
"advanced_settings_upgrade_available": "aggiornamento impostazioni disponibile",
"advanced_settings_upgrade_tip": "Queste sono nuove impostazioni predefinite di 5chan assenti in questo account. Deseleziona ciò che non desideri.",
"advanced_settings_upgrade_http_routers": "aggiungi nuovi router HTTP",
"advanced_settings_upgrade_http_router_aria": "Aggiungi {{router}} ai router HTTP",
"settings_upgrade_keep_current": "mantieni le impostazioni attuali",
"settings_upgrade_apply_selected": "applica modifiche selezionate",
"settings_upgrade_error_saving": "Impossibile salvare l'aggiornamento delle impostazioni.",
"settings_upgrade_review_notice": "aggiornamento impostazioni disponibile",
"settings_upgrade_review_button": "rivedi",
"settings_upgrade_never_show_again": "non mostrare più"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "純粋なP2Pブラウザモード", "advanced_pure_p2p_browser_aria": "純粋なP2Pブラウザモード",
"advanced_options_saved_reloading": "オプションを保存しました。再読み込み中...", "advanced_options_saved_reloading": "オプションを保存しました。再読み込み中...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "オプションの保存中にエラーが発生しました: {{message}}" "advanced_error_saving_options": "オプションの保存中にエラーが発生しました: {{message}}",
"advanced_settings_upgrade_available": "設定のアップグレードが利用可能",
"advanced_settings_upgrade_tip": "これはこのアカウントに不足している新しい5chanのデフォルト設定です。不要なものはチェックを外してください。",
"advanced_settings_upgrade_http_routers": "新しいHTTPルーターを追加",
"advanced_settings_upgrade_http_router_aria": "{{router}} を HTTPルーターに追加",
"settings_upgrade_keep_current": "現在の設定を維持",
"settings_upgrade_apply_selected": "選択した変更を適用",
"settings_upgrade_error_saving": "設定のアップグレードを保存できませんでした。",
"settings_upgrade_review_notice": "設定のアップグレードが利用可能",
"settings_upgrade_review_button": "確認",
"settings_upgrade_never_show_again": "今後表示しない"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "순수 P2P 브라우저 모드", "advanced_pure_p2p_browser_aria": "순수 P2P 브라우저 모드",
"advanced_options_saved_reloading": "옵션이 저장되었습니다. 다시 불러오는 중...", "advanced_options_saved_reloading": "옵션이 저장되었습니다. 다시 불러오는 중...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "옵션 저장 오류: {{message}}" "advanced_error_saving_options": "옵션 저장 오류: {{message}}",
"advanced_settings_upgrade_available": "설정 업그레이드 사용 가능",
"advanced_settings_upgrade_tip": "이 계정에 없는 새로운 5chan 기본값입니다. 원하지 않는 항목은 선택을 해제하세요.",
"advanced_settings_upgrade_http_routers": "새 HTTP 라우터 추가",
"advanced_settings_upgrade_http_router_aria": "{{router}}를 HTTP 라우터에 추가",
"settings_upgrade_keep_current": "현재 설정 유지",
"settings_upgrade_apply_selected": "선택한 변경 사항 적용",
"settings_upgrade_error_saving": "설정 업그레이드를 저장할 수 없습니다.",
"settings_upgrade_review_notice": "설정 업그레이드 사용 가능",
"settings_upgrade_review_button": "검토",
"settings_upgrade_never_show_again": "다시 표시하지 않기"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "शुद्ध P2P ब्राउझर मोड", "advanced_pure_p2p_browser_aria": "शुद्ध P2P ब्राउझर मोड",
"advanced_options_saved_reloading": "पर्याय जतन केले, पुन्हा लोड होत आहे...", "advanced_options_saved_reloading": "पर्याय जतन केले, पुन्हा लोड होत आहे...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "पर्याय जतन करताना त्रुटी: {{message}}" "advanced_error_saving_options": "पर्याय जतन करताना त्रुटी: {{message}}",
"advanced_settings_upgrade_available": "सेटिंग्ज अपग्रेड उपलब्ध",
"advanced_settings_upgrade_tip": "हे या खात्यात नसलेले नवीन 5chan डिफॉल्ट आहेत. जे तुम्हाला नको आहे ते अनचेक करा.",
"advanced_settings_upgrade_http_routers": "नवीन HTTP राऊटर जोडा",
"advanced_settings_upgrade_http_router_aria": "HTTP राऊटरमध्ये {{router}} जोडा",
"settings_upgrade_keep_current": "सध्याच्या सेटिंग्ज ठेवा",
"settings_upgrade_apply_selected": "निवडलेले बदल लागू करा",
"settings_upgrade_error_saving": "सेटिंग्ज अपग्रेड जतन करू शकले नाही.",
"settings_upgrade_review_notice": "सेटिंग्ज अपग्रेड उपलब्ध",
"settings_upgrade_review_button": "पुनरावलोकन",
"settings_upgrade_never_show_again": "पुन्हा दाखवू नका"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Pure P2P-browsermodus", "advanced_pure_p2p_browser_aria": "Pure P2P-browsermodus",
"advanced_options_saved_reloading": "Opties opgeslagen, opnieuw laden...", "advanced_options_saved_reloading": "Opties opgeslagen, opnieuw laden...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Fout bij het opslaan van opties: {{message}}" "advanced_error_saving_options": "Fout bij het opslaan van opties: {{message}}",
"advanced_settings_upgrade_available": "instellingenupgrade beschikbaar",
"advanced_settings_upgrade_tip": "Dit zijn nieuwe 5chan-standaardinstellingen die in dit account ontbreken. Vink alles uit wat je niet wilt.",
"advanced_settings_upgrade_http_routers": "nieuwe HTTP-routers toevoegen",
"advanced_settings_upgrade_http_router_aria": "{{router}} toevoegen aan HTTP-routers",
"settings_upgrade_keep_current": "huidige instellingen behouden",
"settings_upgrade_apply_selected": "geselecteerde wijzigingen toepassen",
"settings_upgrade_error_saving": "De instellingenupgrade kon niet worden opgeslagen.",
"settings_upgrade_review_notice": "instellingenupgrade beschikbaar",
"settings_upgrade_review_button": "bekijken",
"settings_upgrade_never_show_again": "niet meer tonen"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Ren P2P-nettlesermodus", "advanced_pure_p2p_browser_aria": "Ren P2P-nettlesermodus",
"advanced_options_saved_reloading": "Innstillinger lagret, laster på nytt...", "advanced_options_saved_reloading": "Innstillinger lagret, laster på nytt...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Feil ved lagring av alternativer: {{message}}" "advanced_error_saving_options": "Feil ved lagring av alternativer: {{message}}",
"advanced_settings_upgrade_available": "innstillingsoppgradering tilgjengelig",
"advanced_settings_upgrade_tip": "Dette er nye 5chan-standarder som mangler på denne kontoen. Fjern merkingen for alt du ikke ønsker.",
"advanced_settings_upgrade_http_routers": "legg til nye HTTP-rutere",
"advanced_settings_upgrade_http_router_aria": "Legg til {{router}} i HTTP-rutere",
"settings_upgrade_keep_current": "behold gjeldende innstillinger",
"settings_upgrade_apply_selected": "bruk valgte endringer",
"settings_upgrade_error_saving": "Kunne ikke lagre innstillingsoppgraderingen.",
"settings_upgrade_review_notice": "innstillingsoppgradering tilgjengelig",
"settings_upgrade_review_button": "se gjennom",
"settings_upgrade_never_show_again": "vis aldri igjen"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Tryb przeglądarki czystego P2P", "advanced_pure_p2p_browser_aria": "Tryb przeglądarki czystego P2P",
"advanced_options_saved_reloading": "Opcje zapisane, ładowanie ponowne...", "advanced_options_saved_reloading": "Opcje zapisane, ładowanie ponowne...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Błąd podczas zapisywania opcji: {{message}}" "advanced_error_saving_options": "Błąd podczas zapisywania opcji: {{message}}",
"advanced_settings_upgrade_available": "aktualizacja ustawień dostępna",
"advanced_settings_upgrade_tip": "To nowe domyślne ustawienia 5chan, których brakuje na tym koncie. Odznacz wszystko, czego nie chcesz.",
"advanced_settings_upgrade_http_routers": "dodaj nowe routery HTTP",
"advanced_settings_upgrade_http_router_aria": "Dodaj {{router}} do routerów HTTP",
"settings_upgrade_keep_current": "zachowaj obecne ustawienia",
"settings_upgrade_apply_selected": "zastosuj wybrane zmiany",
"settings_upgrade_error_saving": "Nie udało się zapisać aktualizacji ustawień.",
"settings_upgrade_review_notice": "aktualizacja ustawień dostępna",
"settings_upgrade_review_button": "przejrzyj",
"settings_upgrade_never_show_again": "nie pokazuj ponownie"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Modo de navegador P2P puro", "advanced_pure_p2p_browser_aria": "Modo de navegador P2P puro",
"advanced_options_saved_reloading": "Opções salvas, recarregando...", "advanced_options_saved_reloading": "Opções salvas, recarregando...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Erro ao salvar as opções: {{message}}" "advanced_error_saving_options": "Erro ao salvar as opções: {{message}}",
"advanced_settings_upgrade_available": "atualização de configurações disponível",
"advanced_settings_upgrade_tip": "Estas são novas predefinições do 5chan ausentes nesta conta. Desmarque o que não quiser.",
"advanced_settings_upgrade_http_routers": "adicionar novos roteadores HTTP",
"advanced_settings_upgrade_http_router_aria": "Adicionar {{router}} aos roteadores HTTP",
"settings_upgrade_keep_current": "manter configurações atuais",
"settings_upgrade_apply_selected": "aplicar alterações selecionadas",
"settings_upgrade_error_saving": "Não foi possível salvar a atualização das configurações.",
"settings_upgrade_review_notice": "atualização de configurações disponível",
"settings_upgrade_review_button": "revisar",
"settings_upgrade_never_show_again": "não mostrar novamente"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Mod browser P2P pur", "advanced_pure_p2p_browser_aria": "Mod browser P2P pur",
"advanced_options_saved_reloading": "Opțiuni salvate, se reîncarcă...", "advanced_options_saved_reloading": "Opțiuni salvate, se reîncarcă...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Eroare la salvarea opțiunilor: {{message}}" "advanced_error_saving_options": "Eroare la salvarea opțiunilor: {{message}}",
"advanced_settings_upgrade_available": "actualizare setări disponibilă",
"advanced_settings_upgrade_tip": "Acestea sunt noi setări implicite 5chan lipsă din acest cont. Debifați tot ce nu doriți.",
"advanced_settings_upgrade_http_routers": "adaugă noi routere HTTP",
"advanced_settings_upgrade_http_router_aria": "Adaugă {{router}} la routerele HTTP",
"settings_upgrade_keep_current": "păstrează setările actuale",
"settings_upgrade_apply_selected": "aplică modificările selectate",
"settings_upgrade_error_saving": "Nu s-a putut salva actualizarea setărilor.",
"settings_upgrade_review_notice": "actualizare setări disponibilă",
"settings_upgrade_review_button": "revizuiește",
"settings_upgrade_never_show_again": "nu mai afișa"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Режим чистого P2P в браузере", "advanced_pure_p2p_browser_aria": "Режим чистого P2P в браузере",
"advanced_options_saved_reloading": "Настройки сохранены, перезагрузка...", "advanced_options_saved_reloading": "Настройки сохранены, перезагрузка...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Ошибка при сохранении параметров: {{message}}" "advanced_error_saving_options": "Ошибка при сохранении параметров: {{message}}",
"advanced_settings_upgrade_available": "обновление настроек доступно",
"advanced_settings_upgrade_tip": "Это новые настройки 5chan по умолчанию, отсутствующие в этом аккаунте. Снимите галочку с того, что вам не нужно.",
"advanced_settings_upgrade_http_routers": "добавить новые HTTP-маршрутизаторы",
"advanced_settings_upgrade_http_router_aria": "Добавить {{router}} в HTTP-маршрутизаторы",
"settings_upgrade_keep_current": "сохранить текущие настройки",
"settings_upgrade_apply_selected": "применить выбранные изменения",
"settings_upgrade_error_saving": "Не удалось сохранить обновление настроек.",
"settings_upgrade_review_notice": "обновление настроек доступно",
"settings_upgrade_review_button": "просмотреть",
"settings_upgrade_never_show_again": "больше не показывать"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Modalitet shfletuesi P2P i pastër", "advanced_pure_p2p_browser_aria": "Modalitet shfletuesi P2P i pastër",
"advanced_options_saved_reloading": "Opsionet u ruajtën, po ringarkohet...", "advanced_options_saved_reloading": "Opsionet u ruajtën, po ringarkohet...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Gabim gjatë ruajtjes së opsioneve: {{message}}" "advanced_error_saving_options": "Gabim gjatë ruajtjes së opsioneve: {{message}}",
"advanced_settings_upgrade_available": "përmirësim i cilësimeve i disponueshëm",
"advanced_settings_upgrade_tip": "Këto janë parazgjedhje të reja 5chan që mungojnë në këtë llogari. Hiqni shenjën nga çdo gjë që nuk dëshironi.",
"advanced_settings_upgrade_http_routers": "shto routerë HTTP të rinj",
"advanced_settings_upgrade_http_router_aria": "Shto {{router}} te routerët HTTP",
"settings_upgrade_keep_current": "mbaj cilësimet aktuale",
"settings_upgrade_apply_selected": "apliko ndryshimet e zgjedhura",
"settings_upgrade_error_saving": "Nuk u ruajt përmirësimi i cilësimeve.",
"settings_upgrade_review_notice": "përmirësim i cilësimeve i disponueshëm",
"settings_upgrade_review_button": "shqyrto",
"settings_upgrade_never_show_again": "mos e shfaq më"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Rent P2P-webbläsarläge", "advanced_pure_p2p_browser_aria": "Rent P2P-webbläsarläge",
"advanced_options_saved_reloading": "Alternativ sparade, laddar om...", "advanced_options_saved_reloading": "Alternativ sparade, laddar om...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Fel vid sparande av alternativ: {{message}}" "advanced_error_saving_options": "Fel vid sparande av alternativ: {{message}}",
"advanced_settings_upgrade_available": "inställningsuppgradering tillgänglig",
"advanced_settings_upgrade_tip": "Det här är nya 5chan-standardinställningar som saknas i detta konto. Avmarkera allt du inte vill ha.",
"advanced_settings_upgrade_http_routers": "lägg till nya HTTP-routrar",
"advanced_settings_upgrade_http_router_aria": "Lägg till {{router}} i HTTP-routrar",
"settings_upgrade_keep_current": "behåll nuvarande inställningar",
"settings_upgrade_apply_selected": "tillämpa valda ändringar",
"settings_upgrade_error_saving": "Kunde inte spara inställningsuppgraderingen.",
"settings_upgrade_review_notice": "inställningsuppgradering tillgänglig",
"settings_upgrade_review_button": "granska",
"settings_upgrade_never_show_again": "visa inte igen"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "శుద్ధ P2P బ్రౌజర్ మోడ్", "advanced_pure_p2p_browser_aria": "శుద్ధ P2P బ్రౌజర్ మోడ్",
"advanced_options_saved_reloading": "ఎంపికలు సేవ్ చేయబడ్డాయి, మళ్లీ లోడ్ అవుతోంది...", "advanced_options_saved_reloading": "ఎంపికలు సేవ్ చేయబడ్డాయి, మళ్లీ లోడ్ అవుతోంది...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "ఎంపికలు సేవ్ చేయడంలో లోపం: {{message}}" "advanced_error_saving_options": "ఎంపికలు సేవ్ చేయడంలో లోపం: {{message}}",
"advanced_settings_upgrade_available": "సెట్టింగ్‌ల అప్‌గ్రేడ్ అందుబాటులో ఉంది",
"advanced_settings_upgrade_tip": "ఇవి ఈ ఖాతాలో లేని కొత్త 5chan డిఫాల్ట్‌లు. మీకు కావనివి అన్చెక్ చేయండి.",
"advanced_settings_upgrade_http_routers": "నూతన HTTP రౌటర్‌లను జోడించండి",
"advanced_settings_upgrade_http_router_aria": "HTTP రౌటర్‌లకు {{router}} జోడించండి",
"settings_upgrade_keep_current": "ప్రస్తుత అమరికలను ఉంచండి",
"settings_upgrade_apply_selected": "ఎంచుకున్న మార్పులను వర్తింపజేయండి",
"settings_upgrade_error_saving": "సెట్టింగ్‌ల అప్‌గ్రేడ్‌ను సేవ్ చేయలేకపోయింది.",
"settings_upgrade_review_notice": "సెట్టింగ్‌ల అప్‌గ్రేడ్ అందుబాటులో ఉంది",
"settings_upgrade_review_button": "సమీక్షించు",
"settings_upgrade_never_show_again": "మళ్లీ చూపించవద్దు"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "โหมดเบราว์เซอร์ P2P แบบบริสุทธิ์", "advanced_pure_p2p_browser_aria": "โหมดเบราว์เซอร์ P2P แบบบริสุทธิ์",
"advanced_options_saved_reloading": "บันทึกตัวเลือกแล้ว กำลังโหลดใหม่...", "advanced_options_saved_reloading": "บันทึกตัวเลือกแล้ว กำลังโหลดใหม่...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "เกิดข้อผิดพลาดในการบันทึกตัวเลือก: {{message}}" "advanced_error_saving_options": "เกิดข้อผิดพลาดในการบันทึกตัวเลือก: {{message}}",
"advanced_settings_upgrade_available": "มีการอัปเกรดการตั้งค่า",
"advanced_settings_upgrade_tip": "นี่คือค่าเริ่มต้น 5chan ใหม่ที่บัญชีนี้ยังไม่มี ให้ยกเลิกการเลือกสิ่งที่คุณไม่ต้องการ",
"advanced_settings_upgrade_http_routers": "เพิ่มเราเตอร์ HTTP ใหม่",
"advanced_settings_upgrade_http_router_aria": "เพิ่ม {{router}} ในเราเตอร์ HTTP",
"settings_upgrade_keep_current": "คงการตั้งค่าปัจจุบัน",
"settings_upgrade_apply_selected": "ใช้การเปลี่ยนแปลงที่เลือก",
"settings_upgrade_error_saving": "ไม่สามารถบันทึกการอัปเกรดการตั้งค่าได้",
"settings_upgrade_review_notice": "มีการอัปเกรดการตั้งค่า",
"settings_upgrade_review_button": "ตรวจสอบ",
"settings_upgrade_never_show_again": "ไม่ต้องแสดงอีก"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Saf P2P tarayıcı modu", "advanced_pure_p2p_browser_aria": "Saf P2P tarayıcı modu",
"advanced_options_saved_reloading": "Seçenekler kaydedildi, yeniden yükleniyor...", "advanced_options_saved_reloading": "Seçenekler kaydedildi, yeniden yükleniyor...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Seçenekler kaydedilirken hata oluştu: {{message}}" "advanced_error_saving_options": "Seçenekler kaydedilirken hata oluştu: {{message}}",
"advanced_settings_upgrade_available": "ayar yükseltmesi mevcut",
"advanced_settings_upgrade_tip": "Bunlar bu hesapta eksik olan yeni 5chan varsayılanlarıdır. İstemediğiniz her şeyin işaretini kaldırın.",
"advanced_settings_upgrade_http_routers": "yeni HTTP yönlendiricileri ekle",
"advanced_settings_upgrade_http_router_aria": "{{router}} adresini HTTP yönlendiricilerine ekle",
"settings_upgrade_keep_current": "mevcut ayarları koru",
"settings_upgrade_apply_selected": "seçili değişiklikleri uygula",
"settings_upgrade_error_saving": "Ayar yükseltmesi kaydedilemedi.",
"settings_upgrade_review_notice": "ayar yükseltmesi mevcut",
"settings_upgrade_review_button": "incele",
"settings_upgrade_never_show_again": "bir daha gösterme"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Режим чистого P2P у браузері", "advanced_pure_p2p_browser_aria": "Режим чистого P2P у браузері",
"advanced_options_saved_reloading": "Параметри збережено, перезавантаження...", "advanced_options_saved_reloading": "Параметри збережено, перезавантаження...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Помилка збереження параметрів: {{message}}" "advanced_error_saving_options": "Помилка збереження параметрів: {{message}}",
"advanced_settings_upgrade_available": "оновлення налаштувань доступне",
"advanced_settings_upgrade_tip": "Це нові типові налаштування 5chan, яких немає в цьому обліковому записі. Зніміть позначку з того, чого не хочете.",
"advanced_settings_upgrade_http_routers": "додати нові HTTP-маршрутизатори",
"advanced_settings_upgrade_http_router_aria": "Додати {{router}} до HTTP-маршрутизаторів",
"settings_upgrade_keep_current": "зберегти поточні налаштування",
"settings_upgrade_apply_selected": "застосувати вибрані зміни",
"settings_upgrade_error_saving": "Не вдалося зберегти оновлення налаштувань.",
"settings_upgrade_review_notice": "оновлення налаштувань доступне",
"settings_upgrade_review_button": "переглянути",
"settings_upgrade_never_show_again": "більше не показувати"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "خالص P2P براؤزر موڈ", "advanced_pure_p2p_browser_aria": "خالص P2P براؤزر موڈ",
"advanced_options_saved_reloading": "اختیارات محفوظ ہو گئیں، دوبارہ لوڈ ہو رہا ہے...", "advanced_options_saved_reloading": "اختیارات محفوظ ہو گئیں، دوبارہ لوڈ ہو رہا ہے...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "اختیارات محفوظ کرنے میں خرابی: {{message}}" "advanced_error_saving_options": "اختیارات محفوظ کرنے میں خرابی: {{message}}",
"advanced_settings_upgrade_available": "ترتیبات اپ گریڈ دستیاب",
"advanced_settings_upgrade_tip": "یہ نئے 5chan ڈیفالٹ ہیں جو اس اکاؤنٹ میں موجود نہیں۔ جو آپ نہیں چاہتے، ان کا نشان ہٹا دیں۔",
"advanced_settings_upgrade_http_routers": "نئے HTTP روٹرز شامل کریں",
"advanced_settings_upgrade_http_router_aria": "HTTP روٹرز میں {{router}} شامل کریں",
"settings_upgrade_keep_current": "موجودہ ترتیبات برقرار رکھیں",
"settings_upgrade_apply_selected": "منتخب تبدیلیاں لاگو کریں",
"settings_upgrade_error_saving": "ترتیبات اپ گریڈ محفوظ نہیں ہو سکی۔",
"settings_upgrade_review_notice": "ترتیبات اپ گریڈ دستیاب",
"settings_upgrade_review_button": "جائزہ لیں",
"settings_upgrade_never_show_again": "دوبارہ نہ دکھائیں"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "Chế độ trình duyệt P2P thuần túy", "advanced_pure_p2p_browser_aria": "Chế độ trình duyệt P2P thuần túy",
"advanced_options_saved_reloading": "Đã lưu tùy chọn, đang tải lại...", "advanced_options_saved_reloading": "Đã lưu tùy chọn, đang tải lại...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "Lỗi khi lưu tùy chọn: {{message}}" "advanced_error_saving_options": "Lỗi khi lưu tùy chọn: {{message}}",
"advanced_settings_upgrade_available": "nâng cấp cài đặt có sẵn",
"advanced_settings_upgrade_tip": "Đây là các giá trị mặc định mới của 5chan còn thiếu trong tài khoản này. Bỏ chọn những mục bạn không muốn.",
"advanced_settings_upgrade_http_routers": "thêm bộ định tuyến HTTP mới",
"advanced_settings_upgrade_http_router_aria": "Thêm {{router}} vào bộ định tuyến HTTP",
"settings_upgrade_keep_current": "giữ cài đặt hiện tại",
"settings_upgrade_apply_selected": "áp dụng các thay đổi đã chọn",
"settings_upgrade_error_saving": "Không thể lưu bản nâng cấp cài đặt.",
"settings_upgrade_review_notice": "nâng cấp cài đặt có sẵn",
"settings_upgrade_review_button": "xem lại",
"settings_upgrade_never_show_again": "không hiển thị lại"
} }
+11 -1
View File
@@ -432,5 +432,15 @@
"advanced_pure_p2p_browser_aria": "纯 P2P 浏览器模式", "advanced_pure_p2p_browser_aria": "纯 P2P 浏览器模式",
"advanced_options_saved_reloading": "选项已保存,正在重新加载...", "advanced_options_saved_reloading": "选项已保存,正在重新加载...",
"advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>", "advanced_p2p_rpc_placeholder": "ws://<IP>:<port>/<secretAuthKey>",
"advanced_error_saving_options": "保存选项时出错:{{message}}" "advanced_error_saving_options": "保存选项时出错:{{message}}",
"advanced_settings_upgrade_available": "设置升级可用",
"advanced_settings_upgrade_tip": "这些是当前账户缺少的新 5chan 默认设置。取消勾选您不需要的项目。",
"advanced_settings_upgrade_http_routers": "添加新的 HTTP 路由器",
"advanced_settings_upgrade_http_router_aria": "将 {{router}} 添加到 HTTP 路由器",
"settings_upgrade_keep_current": "保留当前设置",
"settings_upgrade_apply_selected": "应用所选更改",
"settings_upgrade_error_saving": "无法保存设置升级。",
"settings_upgrade_review_notice": "设置升级可用",
"settings_upgrade_review_button": "查看",
"settings_upgrade_never_show_again": "不再显示"
} }
+4
View File
@@ -270,6 +270,10 @@ vi.mock('../components/settings-modal', () => ({
default: MockSettingsModal, default: MockSettingsModal,
})); }));
vi.mock('../components/settings-upgrade-modal', () => ({
default: makeNamedComponent('settings-upgrade-modal'),
}));
vi.mock('../components/reply-modal', () => ({ vi.mock('../components/reply-modal', () => ({
default: ({ parentCid, postCid }: { parentCid: string; postCid: string }) => createElement('div', { 'data-testid': 'reply-modal' }, `${parentCid}:${postCid}`), default: ({ parentCid, postCid }: { parentCid: string; postCid: string }) => createElement('div', { 'data-testid': 'reply-modal' }, `${parentCid}:${postCid}`),
})); }));
+4
View File
@@ -63,6 +63,7 @@ const DirectoryModal = lazy(() => import('./components/directory-modal'));
const DisclaimerModal = lazy(() => import('./components/disclaimer-modal')); const DisclaimerModal = lazy(() => import('./components/disclaimer-modal'));
const ReplyModal = lazy(() => import('./components/reply-modal')); const ReplyModal = lazy(() => import('./components/reply-modal'));
const SettingsModal = lazy(() => import('./components/settings-modal')); const SettingsModal = lazy(() => import('./components/settings-modal'));
const SettingsUpgradeModal = lazy(() => import('./components/settings-upgrade-modal'));
// Preload all theme assets (buttons, backgrounds) immediately on app load // Preload all theme assets (buttons, backgrounds) immediately on app load
// to prevent visible loading delays when switching themes // to prevent visible loading delays when switching themes
@@ -225,6 +226,9 @@ const GlobalLayout = () => {
<Suspense fallback={null}> <Suspense fallback={null}>
<ChallengeModal /> <ChallengeModal />
</Suspense> </Suspense>
<Suspense fallback={null}>
<SettingsUpgradeModal />
</Suspense>
{activeCid && threadCid && activeCommunityAddress && ( {activeCid && threadCid && activeCommunityAddress && (
<Suspense fallback={null}> <Suspense fallback={null}>
<ReplyModal <ReplyModal
@@ -4,20 +4,26 @@ import { createRoot, Root } from 'react-dom/client';
import { MemoryRouter, useLocation } from 'react-router-dom'; import { MemoryRouter, useLocation } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import SettingsModal from '../settings-modal'; import SettingsModal from '../settings-modal';
import useSettingsUpgradeReviewStore from '../../../stores/use-settings-upgrade-review-store';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>; const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
const testState = vi.hoisted(() => ({ const testState = vi.hoisted(() => ({
account: { account: {
id: 'account-1',
pkcOptions: { pkcOptions: {
libp2pJsClientsOptions: [{ key: 'libp2pjs' }], libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
}, },
} as Record<string, any>, } as Record<string, any>,
rpcSettings: {
state: 'disconnected',
} as Record<string, any>,
})); }));
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
useAccount: () => testState.account, useAccount: () => testState.account,
usePkcRpcSettings: () => testState.rpcSettings,
})); }));
vi.mock('react-i18next', () => ({ vi.mock('react-i18next', () => ({
@@ -26,39 +32,39 @@ vi.mock('react-i18next', () => ({
}), }),
})); }));
vi.mock('../account-settings', () => ({ vi.mock('../account-settings/account-settings', () => ({
default: () => <div data-testid='account-settings'>account-settings</div>, default: () => <div data-testid='account-settings'>account-settings</div>,
})); }));
vi.mock('../crypto-address-setting', () => ({ vi.mock('../crypto-address-setting/crypto-address-setting', () => ({
default: () => <div data-testid='crypto-address-setting'>crypto-address-setting</div>, default: () => <div data-testid='crypto-address-setting'>crypto-address-setting</div>,
})); }));
vi.mock('../crypto-wallets-setting', () => ({ vi.mock('../crypto-wallets-setting/crypto-wallets-setting', () => ({
default: () => <div data-testid='crypto-wallets-setting'>crypto-wallets-setting</div>, default: () => <div data-testid='crypto-wallets-setting'>crypto-wallets-setting</div>,
})); }));
vi.mock('../interface-settings', () => ({ vi.mock('../interface-settings/interface-settings', () => ({
default: () => <div data-testid='interface-settings-panel'>interface-settings</div>, default: () => <div data-testid='interface-settings-panel'>interface-settings</div>,
})); }));
vi.mock('../media-hosting-settings', () => ({ vi.mock('../media-hosting-settings/media-hosting-settings', () => ({
default: () => <div data-testid='media-hosting-settings-panel'>media-hosting-settings</div>, default: () => <div data-testid='media-hosting-settings-panel'>media-hosting-settings</div>,
})); }));
vi.mock('../advanced-settings', () => ({ vi.mock('../advanced-settings/advanced-settings', () => ({
default: () => <div data-testid='advanced-settings-panel'>advanced-settings</div>, default: () => <div data-testid='advanced-settings-panel'>advanced-settings</div>,
})); }));
vi.mock('../subscriptions-setting', () => ({ vi.mock('../subscriptions-setting/subscriptions-setting', () => ({
default: () => <div data-testid='subscriptions-settings-panel'>subscriptions-settings</div>, default: () => <div data-testid='subscriptions-settings-panel'>subscriptions-settings</div>,
})); }));
vi.mock('../trusted-board-links-setting', () => ({ vi.mock('../trusted-board-links-setting/trusted-board-links-setting', () => ({
default: () => <div data-testid='trusted-board-links-settings-panel'>trusted-board-links-settings</div>, default: () => <div data-testid='trusted-board-links-settings-panel'>trusted-board-links-settings</div>,
})); }));
vi.mock('../p2p-stats-settings', () => ({ vi.mock('../p2p-stats-settings/p2p-stats-settings', () => ({
default: () => <div data-testid='p2p-stats-settings-panel'>p2p-stats-settings</div>, default: () => <div data-testid='p2p-stats-settings-panel'>p2p-stats-settings</div>,
})); }));
@@ -91,6 +97,22 @@ const getSectionToggleByText = (text: string) => {
describe('SettingsModal', () => { describe('SettingsModal', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
localStorage.clear();
testState.account = {
id: 'account-1',
pkcOptions: {
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
};
testState.rpcSettings = {
state: 'disconnected',
};
useSettingsUpgradeReviewStore.setState({
hiddenReviewUpgradeKeys: [],
persistentDismissedUpgradeKeys: [],
reviewedUpgradeKeys: [],
reviewRequestId: 0,
});
container = document.createElement('div'); container = document.createElement('div');
document.body.appendChild(container); document.body.appendChild(container);
root = createRoot(container); root = createRoot(container);
@@ -175,6 +197,42 @@ describe('SettingsModal', () => {
expect(container.querySelector('[data-testid="p2p-stats-settings-panel"]')).toBeNull(); expect(container.querySelector('[data-testid="p2p-stats-settings-panel"]')).toBeNull();
}); });
it('shows a review button for dismissed settings upgrades', async () => {
const upgradeKey = 'account-1:http-routers:https://routerofbitsocial.xyz|https://bsotracker.online';
testState.account = {
id: 'account-1',
pkcOptions: {
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
};
useSettingsUpgradeReviewStore.getState().dismissUpgradeKeys([upgradeKey]);
render('/all/settings');
expect(container.textContent).toContain('settings_upgrade_review_notice');
const reviewButton = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === 'settings_upgrade_review_button');
if (!reviewButton) {
throw new Error('settings upgrade review button not found');
}
const reviewBanner = container.querySelector('[data-testid="settings-upgrade-review-banner"]');
const settingsDialog = container.querySelector('dialog[aria-labelledby="settings-modal-title"]');
expect(reviewBanner).not.toBeNull();
expect(reviewBanner?.textContent).toBe('settings_upgrade_review_notice [settings_upgrade_review_button]');
expect(reviewBanner?.querySelector('br')).toBeNull();
expect(reviewBanner?.textContent).not.toContain('settings_upgrade_never_show_again');
expect(settingsDialog?.lastElementChild).toBe(reviewBanner);
await act(async () => {
reviewButton.dispatchEvent(new MouseEvent('click', { bubbles: true }));
});
expect(localStorage.getItem('5chan:dismissed-settings-upgrades')).not.toContain(upgradeKey);
expect(localStorage.getItem('5chan:hidden-settings-upgrade-reviews')).not.toContain(upgradeKey);
expect(useSettingsUpgradeReviewStore.getState().reviewRequestId).toBe(1);
});
it('opens the p2p stats section from its hash when browser pure p2p is enabled', () => { it('opens the p2p stats section from its hash when browser pure p2p is enabled', () => {
render('/all/settings#p2p-stats-settings'); render('/all/settings#p2p-stats-settings');
@@ -8,6 +8,7 @@ import AccountSettings from '../account-settings';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>; const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
const IMPORTED_ACCOUNT_ADDRESSES_STORAGE_KEY = 'importedAccountAddresses'; const IMPORTED_ACCOUNT_ADDRESSES_STORAGE_KEY = 'importedAccountAddresses';
const LEGACY_DEFAULT_HTTP_ROUTERS = ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'];
const hookMocks = vi.hoisted(() => ({ const hookMocks = vi.hoisted(() => ({
deleteAccount: vi.fn(), deleteAccount: vi.fn(),
@@ -343,6 +344,7 @@ describe('AccountSettings', () => {
expect(hookMocks.importAccount).toHaveBeenCalledOnce(); expect(hookMocks.importAccount).toHaveBeenCalledOnce();
const importedPayload = JSON.parse(hookMocks.importAccount.mock.calls[0][0]); const importedPayload = JSON.parse(hookMocks.importAccount.mock.calls[0][0]);
expect(importedPayload.account.subscriptions).toEqual(['business.eth', 'music-posting.bso']); expect(importedPayload.account.subscriptions).toEqual(['business.eth', 'music-posting.bso']);
expect(importedPayload.account.pkcOptions.httpRoutersOptions).toEqual(LEGACY_DEFAULT_HTTP_ROUTERS);
expect(localStorage.getItem(IMPORTED_ACCOUNT_ADDRESSES_STORAGE_KEY)).toBe(JSON.stringify(['0x999'])); expect(localStorage.getItem(IMPORTED_ACCOUNT_ADDRESSES_STORAGE_KEY)).toBe(JSON.stringify(['0x999']));
expect(localStorage.getItem('importedAccountAddress')).toBe('0x999'); expect(localStorage.getItem('importedAccountAddress')).toBe('0x999');
expect(hookMocks.setActiveAccount).toHaveBeenCalledWith('Imported'); expect(hookMocks.setActiveAccount).toHaveBeenCalledWith('Imported');
@@ -350,6 +352,74 @@ describe('AccountSettings', () => {
expect(getLocationText()).toBe('/subs/settings#account-settings'); expect(getLocationText()).toBe('/subs/settings#account-settings');
}); });
it('preserves explicit HTTP routers when importing an account backup', async () => {
fileReaderState.result = JSON.stringify({
account: {
name: 'Imported',
author: { address: '0x999' },
pkcOptions: {
httpRoutersOptions: ['https://router.custom.example'],
ipfsGatewayUrls: ['https://gateway.custom.example'],
},
},
});
hookMocks.importAccount.mockResolvedValue(undefined);
hookMocks.setActiveAccount.mockResolvedValue(undefined);
render();
await act(async () => {
getButtonByText('import_account_backup').click();
});
const file = new File(['{}'], 'account.json', { type: 'application/json' });
await act(async () => {
createdInput?.onchange?.({ target: { files: [file] } } as unknown as Event);
await Promise.resolve();
});
await flushMicrotasks();
const importedPayload = JSON.parse(hookMocks.importAccount.mock.calls[0][0]);
expect(importedPayload.account.pkcOptions).toEqual({
httpRoutersOptions: ['https://router.custom.example'],
ipfsGatewayUrls: ['https://gateway.custom.example'],
});
});
it('preserves an explicit empty HTTP routers list when importing an account backup', async () => {
fileReaderState.result = JSON.stringify({
account: {
name: 'Imported',
author: { address: '0x999' },
pkcOptions: {
httpRoutersOptions: [],
ipfsGatewayUrls: ['https://gateway.custom.example'],
},
},
});
hookMocks.importAccount.mockResolvedValue(undefined);
hookMocks.setActiveAccount.mockResolvedValue(undefined);
render();
await act(async () => {
getButtonByText('import_account_backup').click();
});
const file = new File(['{}'], 'account.json', { type: 'application/json' });
await act(async () => {
createdInput?.onchange?.({ target: { files: [file] } } as unknown as Event);
await Promise.resolve();
});
await flushMicrotasks();
const importedPayload = JSON.parse(hookMocks.importAccount.mock.calls[0][0]);
expect(importedPayload.account.pkcOptions).toEqual({
httpRoutersOptions: [],
ipfsGatewayUrls: ['https://gateway.custom.example'],
});
});
it('activates the resolved account name when an imported account name already exists', async () => { it('activates the resolved account name when an imported account name already exists', async () => {
hookMocks.useAccounts.mockReturnValue({ hookMocks.useAccounts.mockReturnValue({
accounts: [ accounts: [
@@ -4,6 +4,7 @@ import { deleteAccount, exportAccount, importAccount, setActiveAccount, useAccou
import styles from './account-settings.module.css'; import styles from './account-settings.module.css';
import { Capacitor } from '@capacitor/core'; import { Capacitor } from '@capacitor/core';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { getLegacyDefaultBrowserHttpRoutersOptions } from '../../../lib/p2p-browser-config';
const isAndroid = Capacitor.getPlatform() === 'android'; const isAndroid = Capacitor.getPlatform() === 'android';
const IMPORTED_ACCOUNT_ADDRESSES_STORAGE_KEY = 'importedAccountAddresses'; const IMPORTED_ACCOUNT_ADDRESSES_STORAGE_KEY = 'importedAccountAddresses';
@@ -64,6 +65,18 @@ const getImportedAccountActiveName = (importedAccountName: string | undefined, a
return accounts.some((account) => account?.name === importedAccountName) ? `${importedAccountName} 2` : importedAccountName; return accounts.some((account) => account?.name === importedAccountName) ? `${importedAccountName} 2` : importedAccountName;
}; };
type ImportedAccountProtocolOptions = Record<string, unknown> & {
httpRoutersOptions?: unknown;
};
const toImportedAccountProtocolOptions = (protocolOptions: unknown): ImportedAccountProtocolOptions =>
protocolOptions && typeof protocolOptions === 'object' && !Array.isArray(protocolOptions) ? (protocolOptions as ImportedAccountProtocolOptions) : {};
const hasExplicitHttpRoutersOptions = (protocolOptions: unknown) => {
const httpRoutersOptions = toImportedAccountProtocolOptions(protocolOptions).httpRoutersOptions;
return Array.isArray(httpRoutersOptions);
};
// Inner component keyed by account id so state resets when user switches account // Inner component keyed by account id so state resets when user switches account
const AccountSettingsEditor = ({ const AccountSettingsEditor = ({
account, account,
@@ -139,7 +152,13 @@ const AccountSettingsEditor = ({
} }
const accountData = safeParseJSON<{ const accountData = safeParseJSON<{
account?: { communities?: Record<string, unknown>; subscriptions?: string[]; author?: { address?: string }; name?: string }; account?: {
communities?: Record<string, unknown>;
subscriptions?: string[];
author?: { address?: string };
name?: string;
pkcOptions?: ImportedAccountProtocolOptions;
};
}>(fileContent); }>(fileContent);
if (!accountData) { if (!accountData) {
alert('Invalid JSON in file.'); alert('Invalid JSON in file.');
@@ -162,6 +181,13 @@ const AccountSettingsEditor = ({
accountData.account.subscriptions = uniqueSubscriptions; accountData.account.subscriptions = uniqueSubscriptions;
} }
if (accountData.account && !hasExplicitHttpRoutersOptions(accountData.account.pkcOptions)) {
accountData.account.pkcOptions = {
...toImportedAccountProtocolOptions(accountData.account.pkcOptions),
httpRoutersOptions: getLegacyDefaultBrowserHttpRoutersOptions(),
};
}
const modifiedAccountJson = JSON.stringify(accountData); const modifiedAccountJson = JSON.stringify(accountData);
const importedAccountActiveName = getImportedAccountActiveName(accountData.account?.name, accounts); const importedAccountActiveName = getImportedAccountActiveName(accountData.account?.name, accounts);
const result = await withErrorHandling( const result = await withErrorHandling(
@@ -220,6 +220,22 @@ describe('AdvancedSettings', () => {
expect(nodeRpcInput).toBeTruthy(); expect(nodeRpcInput).toBeTruthy();
}); });
it('keeps settings upgrade review out of advanced settings', async () => {
testState.account = {
mediaIpfsGatewayUrl: 'https://media.old.example',
pkcOptions: {
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
};
await renderSettings(false);
expect(container.textContent).not.toContain('advanced_settings_upgrade_available');
expect(container.textContent).not.toContain('https://routerofbitsocial.xyz');
expect(container.textContent).not.toContain('https://bsotracker.online');
});
it('saves browser pure p2p settings when the toggle is turned on', async () => { it('saves browser pure p2p settings when the toggle is turned on', async () => {
localStorage.setItem('5chan:pure-p2p-browser-enabled', 'false'); localStorage.setItem('5chan:pure-p2p-browser-enabled', 'false');
@@ -293,7 +293,7 @@ const AdvancedSettings = () => {
const ethRpcUrls = getTrimmedLines(ethRpcRef.current?.value); const ethRpcUrls = getTrimmedLines(ethRpcRef.current?.value);
const httpRoutersOptions = getTrimmedLines(httpRoutersRef.current?.value); const httpRoutersOptions = httpRoutersRef.current ? getTrimmedLines(httpRoutersRef.current.value) : protocolOptions?.httpRoutersOptions;
const pkcRpcClientsOptions = p2pRpcRef.current?.value.trim() ? [p2pRpcRef.current.value.trim()] : undefined; const pkcRpcClientsOptions = p2pRpcRef.current?.value.trim() ? [p2pRpcRef.current.value.trim()] : undefined;
const dataPath = p2pDataPathRef.current?.value.trim() || undefined; const dataPath = p2pDataPathRef.current?.value.trim() || undefined;
@@ -99,6 +99,24 @@
text-transform: capitalize; text-transform: capitalize;
} }
.settingsUpgradeNotice {
position: sticky;
bottom: 0;
z-index: 1;
display: block;
box-sizing: border-box;
width: 100%;
margin: 8px 0 0;
padding: 3px 5px;
background-color: #E62020;
color: white;
font-family: monospace;
font-size: 12px;
text-align: center;
text-shadow: 0 1px rgba(0, 0, 0, 0.20);
text-transform: none;
}
.expandAllSettings span, .expandAllSettings button { .expandAllSettings span, .expandAllSettings button {
appearance: none; appearance: none;
background: transparent; background: transparent;
@@ -109,11 +127,29 @@
padding: 0; padding: 0;
} }
.settingsUpgradeNotice button {
appearance: none;
background: transparent;
border: 0;
color: inherit;
filter: none;
font: inherit;
margin: 0;
padding: 0;
text-decoration: underline;
text-transform: none;
}
.expandAllSettings span:hover, .expandAllSettings button:hover { .expandAllSettings span:hover, .expandAllSettings button:hover {
color: var(--button-desktop-text-color-hover); color: var(--button-desktop-text-color-hover);
cursor: pointer; cursor: pointer;
} }
.settingsUpgradeNotice button:hover {
color: inherit;
cursor: pointer;
}
.setting { .setting {
margin-bottom: 8px; margin-bottom: 8px;
text-transform: capitalize; text-transform: capitalize;
@@ -1,18 +1,20 @@
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAccount } from '@bitsocial/bitsocial-react-hooks'; import { useAccount, usePkcRpcSettings } from '@bitsocial/bitsocial-react-hooks';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import styles from './settings-modal.module.css'; import styles from './settings-modal.module.css';
import AccountSettings from './account-settings'; import AccountSettings from './account-settings/account-settings';
import CryptoAddressSetting from './crypto-address-setting'; import CryptoAddressSetting from './crypto-address-setting/crypto-address-setting';
import CryptoWalletsSetting from './crypto-wallets-setting'; import CryptoWalletsSetting from './crypto-wallets-setting/crypto-wallets-setting';
import InterfaceSettings from './interface-settings'; import InterfaceSettings from './interface-settings/interface-settings';
import MediaHostingSettings from './media-hosting-settings'; import MediaHostingSettings from './media-hosting-settings/media-hosting-settings';
import AdvancedSettings from './advanced-settings'; import AdvancedSettings from './advanced-settings/advanced-settings';
import SubscriptionsSetting from './subscriptions-setting'; import SubscriptionsSetting from './subscriptions-setting/subscriptions-setting';
import TrustedBoardLinksSetting from './trusted-board-links-setting'; import TrustedBoardLinksSetting from './trusted-board-links-setting/trusted-board-links-setting';
import P2PStatsSettings from './p2p-stats-settings'; import P2PStatsSettings from './p2p-stats-settings/p2p-stats-settings';
import { P2P_STATS_SECTION_ID, shouldShowP2PSettingsSection } from '../../lib/p2p-runtime'; import { P2P_STATS_SECTION_ID, shouldShowP2PSettingsSection } from '../../lib/p2p-runtime';
import { getReviewableSettingsUpgrades, getSettingsUpgradeKey, type SettingsUpgradeAccount } from '../../lib/settings-upgrades';
import useSettingsUpgradeReviewStore from '../../stores/use-settings-upgrade-review-store';
const allSectionIds = [ const allSectionIds = [
'interface-settings', 'interface-settings',
@@ -32,11 +34,22 @@ const hashToSection = (hash: string, sectionIds = allSectionIds): string | null
const SettingsModal = () => { const SettingsModal = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const account = useAccount(); const account = useAccount();
const pkcRpc = usePkcRpcSettings();
const { hash: locationHash, pathname } = useLocation(); const { hash: locationHash, pathname } = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const hiddenReviewUpgradeKeys = useSettingsUpgradeReviewStore((state) => state.hiddenReviewUpgradeKeys);
const reviewUpgradeKeys = useSettingsUpgradeReviewStore((state) => state.reviewUpgradeKeys);
const hash = locationHash.slice(1); const hash = locationHash.slice(1);
const sectionIds = useMemo(() => (shouldShowP2PSettingsSection(account) ? [...allSectionIds, P2P_STATS_SECTION_ID] : allSectionIds), [account]); const sectionIds = useMemo(() => (shouldShowP2PSettingsSection(account) ? [...allSectionIds, P2P_STATS_SECTION_ID] : allSectionIds), [account]);
const hashSection = hashToSection(hash, sectionIds); const hashSection = hashToSection(hash, sectionIds);
const settingsUpgradeKeys = useMemo(() => {
if (!account || pkcRpc?.state === 'connected') return [];
const settingsUpgradeAccount = account as SettingsUpgradeAccount;
return getReviewableSettingsUpgrades(settingsUpgradeAccount).map((upgrade) => getSettingsUpgradeKey(settingsUpgradeAccount, upgrade));
}, [account, pkcRpc?.state]);
const visibleSettingsUpgradeKeys = settingsUpgradeKeys.filter((upgradeKey) => !hiddenReviewUpgradeKeys.includes(upgradeKey));
const showSettingsUpgradeReview = visibleSettingsUpgradeKeys.length > 0;
const closeModal = useCallback(() => { const closeModal = useCallback(() => {
const newPath = pathname.replace(/\/settings$/, ''); const newPath = pathname.replace(/\/settings$/, '');
@@ -109,6 +122,10 @@ const SettingsModal = () => {
} }
}; };
const handleReviewSettingsUpgrades = () => {
reviewUpgradeKeys(visibleSettingsUpgradeKeys);
};
const handleKeyDown = (handler: () => void) => (e: React.KeyboardEvent) => { const handleKeyDown = (handler: () => void) => (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') { if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault(); e.preventDefault();
@@ -202,6 +219,16 @@ const SettingsModal = () => {
{showP2PStatsSettings && <P2PStatsSettings />} {showP2PStatsSettings && <P2PStatsSettings />}
</> </>
)} )}
{showSettingsUpgradeReview && (
<output className={styles.settingsUpgradeNotice} aria-live='polite' data-testid='settings-upgrade-review-banner'>
<span>{t('settings_upgrade_review_notice')}</span>
{' ['}
<button type='button' tabIndex={0} onClick={handleReviewSettingsUpgrades} onKeyDown={handleKeyDown(handleReviewSettingsUpgrades)}>
{t('settings_upgrade_review_button')}
</button>
{']'}
</output>
)}
</dialog> </dialog>
</> </>
); );
@@ -0,0 +1,232 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
const testState = vi.hoisted(() => ({
account: {
id: 'account-1',
name: 'Imported',
mediaIpfsGatewayUrl: 'https://media.example',
pkcOptions: {
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
} as Record<string, any> | undefined,
rpcSettings: {
state: 'disconnected',
} as Record<string, any>,
setAccountMock: vi.fn().mockResolvedValue(undefined),
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
setAccount: (account: unknown) => testState.setAccountMock(account),
useAccount: () => testState.account,
usePkcRpcSettings: () => testState.rpcSettings,
}));
let alertSpy: ReturnType<typeof vi.spyOn>;
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let container: HTMLDivElement;
const originalLocation = window.location;
let reloadMock: ReturnType<typeof vi.fn>;
let root: Root;
const renderModal = async () => {
const SettingsUpgradeModal = (await import('../settings-upgrade-modal')).default;
await act(async () => {
root.render(createElement(SettingsUpgradeModal));
});
};
const getUpgradeCheckbox = (routerUrl: string) => {
const label = Array.from(container.querySelectorAll('label')).find((candidate) => candidate.textContent?.includes(routerUrl));
return label?.querySelector<HTMLInputElement>('input[type="checkbox"]');
};
const clickButton = async (text: string) => {
const button = Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === text);
await act(async () => {
button?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
await Promise.resolve();
});
};
describe('SettingsUpgradeModal', () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
localStorage.clear();
testState.account = {
id: 'account-1',
name: 'Imported',
mediaIpfsGatewayUrl: 'https://media.example',
pkcOptions: {
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
};
testState.rpcSettings = {
state: 'disconnected',
};
testState.setAccountMock.mockReset().mockResolvedValue(undefined);
reloadMock = vi.fn();
Object.defineProperty(window, 'location', {
configurable: true,
value: {
...originalLocation,
reload: reloadMock,
},
});
alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => undefined);
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
if (root) {
act(() => root.unmount());
}
container?.remove();
Object.defineProperty(window, 'location', {
configurable: true,
value: originalLocation,
});
alertSpy?.mockRestore();
consoleLogSpy?.mockRestore();
});
it('opens automatically when an active account has reviewable HTTP tracker upgrades', async () => {
await renderModal();
expect(container.textContent).toContain('advanced_settings_upgrade_available');
expect(container.textContent).toContain('advanced_settings_upgrade_tip');
expect(container.textContent).toContain('https://routerofbitsocial.xyz');
expect(container.textContent).toContain('https://bsotracker.online');
expect(container.textContent).not.toContain('settings_upgrade_never_show_again');
expect(getUpgradeCheckbox('https://routerofbitsocial.xyz')?.checked).toBe(true);
expect(getUpgradeCheckbox('https://bsotracker.online')?.checked).toBe(true);
});
it('applies only selected HTTP tracker upgrades', async () => {
await renderModal();
await act(async () => {
getUpgradeCheckbox('https://bsotracker.online')?.click();
});
await clickButton('settings_upgrade_apply_selected');
expect(testState.setAccountMock).toHaveBeenCalledWith(
expect.objectContaining({
mediaIpfsGatewayUrl: 'https://media.example',
pkcOptions: expect.objectContaining({
httpRoutersOptions: [
'https://routing.lol',
'https://peers.pleb.bot',
'https://peers.plebpubsub.xyz',
'https://peers.forumindex.com',
'https://routerofbitsocial.xyz',
],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
}),
}),
);
expect(reloadMock).toHaveBeenCalledOnce();
});
it('remembers when the user keeps current settings', async () => {
await renderModal();
await clickButton('settings_upgrade_keep_current');
expect(container.querySelector('dialog')).toBeNull();
expect(testState.setAccountMock).not.toHaveBeenCalled();
expect(localStorage.getItem('5chan:dismissed-settings-upgrades')).toContain('account-1:http-routers:https://routerofbitsocial.xyz|https://bsotracker.online');
await renderModal();
expect(container.querySelector('dialog')).toBeNull();
});
it('reopens dismissed upgrades when the user reviews them from settings', async () => {
await renderModal();
await clickButton('settings_upgrade_keep_current');
expect(container.querySelector('dialog')).toBeNull();
const useSettingsUpgradeReviewStore = (await import('../../../stores/use-settings-upgrade-review-store')).default;
await act(async () => {
useSettingsUpgradeReviewStore.getState().reviewUpgradeKeys(['account-1:http-routers:https://routerofbitsocial.xyz|https://bsotracker.online']);
});
expect(container.querySelector('dialog')).not.toBeNull();
expect(container.textContent).toContain('https://routerofbitsocial.xyz');
expect(localStorage.getItem('5chan:dismissed-settings-upgrades')).not.toContain('account-1:http-routers:https://routerofbitsocial.xyz|https://bsotracker.online');
expect(localStorage.getItem('5chan:hidden-settings-upgrade-reviews')).not.toContain('account-1:http-routers:https://routerofbitsocial.xyz|https://bsotracker.online');
});
it('shows the permanent hide action only after the upgrade is reopened from settings review', async () => {
const upgradeKey = 'account-1:http-routers:https://routerofbitsocial.xyz|https://bsotracker.online';
await renderModal();
expect(container.textContent).not.toContain('settings_upgrade_never_show_again');
await clickButton('settings_upgrade_keep_current');
const useSettingsUpgradeReviewStore = (await import('../../../stores/use-settings-upgrade-review-store')).default;
await act(async () => {
useSettingsUpgradeReviewStore.getState().reviewUpgradeKeys([upgradeKey]);
});
expect(container.querySelector('dialog')).not.toBeNull();
expect(container.textContent).toContain('settings_upgrade_never_show_again');
expect(
Array.from(container.querySelectorAll('button')).find((candidate) => candidate.textContent === 'settings_upgrade_never_show_again')?.parentElement?.textContent,
).toBe('settings_upgrade_never_show_again');
await clickButton('settings_upgrade_never_show_again');
expect(container.querySelector('dialog')).toBeNull();
expect(localStorage.getItem('5chan:dismissed-settings-upgrades')).toContain(upgradeKey);
expect(localStorage.getItem('5chan:hidden-settings-upgrade-reviews')).toContain(upgradeKey);
});
it('does not reopen upgrade prompts hidden from settings review', async () => {
localStorage.setItem('5chan:hidden-settings-upgrade-reviews', JSON.stringify(['account-1:http-routers:https://routerofbitsocial.xyz|https://bsotracker.online']));
await renderModal();
expect(container.querySelector('dialog')).toBeNull();
});
it('stays hidden for custom routers and connected RPC settings', async () => {
testState.account = {
id: 'account-1',
pkcOptions: {
httpRoutersOptions: ['https://router.custom.example'],
},
};
await renderModal();
expect(container.querySelector('dialog')).toBeNull();
testState.account = {
id: 'account-2',
pkcOptions: {
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
},
};
testState.rpcSettings = { state: 'connected' };
await renderModal();
expect(container.querySelector('dialog')).toBeNull();
});
});
@@ -0,0 +1 @@
export { default } from './settings-upgrade-modal';
@@ -0,0 +1,137 @@
.backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.25);
z-index: 1200;
}
.backdropButton {
all: unset;
position: absolute;
inset: 0;
cursor: default;
}
.dialog {
position: fixed;
top: 75px;
left: 50%;
transform: translateX(-50%);
box-sizing: border-box;
width: 420px;
max-width: calc(100vw - 20px);
max-height: calc(100vh - 30px);
overflow-y: auto;
margin: 0;
padding: 2px;
background-color: var(--settings-modal-background-color);
border-top: var(--settings-modal-border-top);
border-right: var(--settings-modal-border-right);
border-bottom: var(--settings-modal-border-bottom);
border-left: var(--settings-modal-border-left);
color: inherit;
font-size: var(--settings-modal-font-size);
z-index: 1201;
}
.header {
position: relative;
margin: 5px 0;
padding-bottom: 5px;
text-align: center;
border-bottom: var(--settings-modal-header-border-bottom);
}
.header h2 {
margin: 0;
padding: 0;
font-size: 16px;
font-weight: 700;
line-height: 14px;
text-transform: capitalize;
}
.closeButton {
appearance: none;
position: absolute;
top: -2px;
right: 3px;
display: block;
width: 18px;
height: 18px;
margin: 0;
padding: 0;
border: 0;
background-color: transparent;
background-image: var(--settings-modal-close-button-background-image);
background-position: center;
background-repeat: no-repeat;
image-rendering: pixelated;
cursor: pointer;
}
.body {
padding: 3px 5px 5px;
}
.tip {
margin: 0 0 8px;
}
.options {
margin: 0;
padding: 4px 5px 5px;
border: var(--settings-modal-border-bottom);
}
.options + .options {
margin-top: 6px;
}
.options legend {
padding: 0 2px;
font-weight: 700;
text-transform: capitalize;
}
.option {
display: block;
margin-top: 4px;
font-family: monospace;
font-size: 13px;
overflow-wrap: anywhere;
cursor: pointer;
}
.option input {
margin: 0 5px 0 0;
}
.actions {
margin-top: 10px;
text-align: center;
}
.actions button {
margin: 0 3px;
text-transform: capitalize;
cursor: pointer;
}
.neverShowAgainAction {
margin-top: 14px;
text-align: center;
}
.neverShowAgainAction button {
margin: 0 3px;
text-transform: capitalize;
cursor: pointer;
}
@media (max-width: 640px) {
.dialog {
top: 20px;
width: calc(100vw - 20px);
}
}
@@ -0,0 +1,165 @@
import { useState } from 'react';
import { setAccount, useAccount, usePkcRpcSettings } from '@bitsocial/bitsocial-react-hooks';
import { useTranslation } from 'react-i18next';
import {
applySelectedSettingsUpgrades,
getReviewableSettingsUpgrades,
getSelectedSettingsUpgradeOptionCount,
getSettingsUpgradeKey,
getSettingsUpgradeOptionSelectionKey,
isSettingsUpgradeOptionSelected,
type ReviewableSettingsUpgrade,
type SettingsUpgradeAccount,
type SettingsUpgradeSelections,
} from '../../lib/settings-upgrades';
import useSettingsUpgradeReviewStore from '../../stores/use-settings-upgrade-review-store';
import styles from './settings-upgrade-modal.module.css';
const SettingsUpgradeModalContent = ({
account,
allowPermanentHide,
dismissUpgrades,
hideUpgrades,
upgradeKeys,
upgrades,
}: {
account: SettingsUpgradeAccount;
allowPermanentHide: boolean;
dismissUpgrades: (upgradeKeys: string[], persist: boolean) => void;
hideUpgrades: (upgradeKeys: string[]) => void;
upgradeKeys: string[];
upgrades: ReviewableSettingsUpgrade[];
}) => {
const { t } = useTranslation();
const [upgradeSelections, setUpgradeSelections] = useState<SettingsUpgradeSelections>({});
const selectedUpgradeOptionCount = getSelectedSettingsUpgradeOptionCount(upgrades, upgradeSelections);
const handleSelectionChange = (upgrade: ReviewableSettingsUpgrade, optionId: string, selected: boolean) => {
const option = upgrade.options.find((candidate) => candidate.id === optionId);
if (!option) return;
setUpgradeSelections((selections) => ({
...selections,
[getSettingsUpgradeOptionSelectionKey(upgrade, option)]: selected,
}));
};
const handleKeepCurrent = () => {
dismissUpgrades(upgradeKeys, true);
};
const handleNeverShowAgain = () => {
hideUpgrades(upgradeKeys);
};
const handleApplySelected = async () => {
if (selectedUpgradeOptionCount === 0) {
dismissUpgrades(upgradeKeys, true);
return;
}
try {
await setAccount(applySelectedSettingsUpgrades(account, upgrades, upgradeSelections));
window.location.reload();
} catch (error) {
alert(t('settings_upgrade_error_saving'));
if (error instanceof Error) {
console.log(error);
}
}
};
return (
<div className={styles.backdrop}>
<button type='button' className={styles.backdropButton} aria-label={t('close')} onClick={() => dismissUpgrades(upgradeKeys, false)} />
<dialog open className={styles.dialog} aria-modal='true' aria-labelledby='settings-upgrade-modal-title'>
<div className={styles.header}>
<h2 id='settings-upgrade-modal-title'>{t('advanced_settings_upgrade_available')}</h2>
<button type='button' className={styles.closeButton} aria-label={t('close')} title={t('close')} onClick={() => dismissUpgrades(upgradeKeys, false)} />
</div>
<div className={styles.body}>
<p className={styles.tip}>{t('advanced_settings_upgrade_tip')}</p>
{upgrades.map((upgrade) => (
<fieldset className={styles.options} key={upgrade.id}>
<legend>{t(upgrade.labelKey)}</legend>
{upgrade.options.map((option) => (
<label className={styles.option} key={option.id}>
<input
type='checkbox'
aria-label={t(option.ariaLabelKey, option.ariaLabelValues)}
checked={isSettingsUpgradeOptionSelected(upgrade, option, upgradeSelections)}
onChange={(event) => handleSelectionChange(upgrade, option.id, event.currentTarget.checked)}
/>
{option.label}
</label>
))}
</fieldset>
))}
<div className={styles.actions}>
<button type='button' onClick={handleKeepCurrent}>
{t('settings_upgrade_keep_current')}
</button>
<button type='button' onClick={handleApplySelected} disabled={selectedUpgradeOptionCount === 0}>
{t('settings_upgrade_apply_selected')}
</button>
</div>
{allowPermanentHide && (
<div className={styles.neverShowAgainAction}>
<button type='button' onClick={handleNeverShowAgain}>
{t('settings_upgrade_never_show_again')}
</button>
</div>
)}
</div>
</dialog>
</div>
);
};
const SettingsUpgradeModal = () => {
const account = useAccount() as SettingsUpgradeAccount | undefined;
const pkcRpc = usePkcRpcSettings();
const { dismissUpgradeKeys, hiddenReviewUpgradeKeys, hideReviewUpgradeKeys, persistentDismissedUpgradeKeys, reviewedUpgradeKeys, reviewRequestId } =
useSettingsUpgradeReviewStore();
const [sessionDismissedUpgradeKeys, setSessionDismissedUpgradeKeys] = useState<{ reviewRequestId: number; upgradeKeys: string[] }>(() => ({
reviewRequestId,
upgradeKeys: [],
}));
if (!account || pkcRpc?.state === 'connected') return null;
const activeSessionDismissedUpgradeKeys = sessionDismissedUpgradeKeys.reviewRequestId === reviewRequestId ? sessionDismissedUpgradeKeys.upgradeKeys : [];
const dismissedUpgradeKeys = new Set([...persistentDismissedUpgradeKeys, ...hiddenReviewUpgradeKeys, ...activeSessionDismissedUpgradeKeys]);
const settingsUpgrades = getReviewableSettingsUpgrades(account).filter((upgrade) => !dismissedUpgradeKeys.has(getSettingsUpgradeKey(account, upgrade)));
if (settingsUpgrades.length === 0) return null;
const upgradeKeys = settingsUpgrades.map((upgrade) => getSettingsUpgradeKey(account, upgrade));
const reviewedUpgradeKeySet = new Set(reviewedUpgradeKeys);
const allowPermanentHide = upgradeKeys.some((upgradeKey) => reviewedUpgradeKeySet.has(upgradeKey));
const dismissUpgrades = (nextUpgradeKeys: string[], persist: boolean) => {
if (persist) {
dismissUpgradeKeys(nextUpgradeKeys);
return;
}
setSessionDismissedUpgradeKeys({
reviewRequestId,
upgradeKeys: [...new Set([...activeSessionDismissedUpgradeKeys, ...nextUpgradeKeys])],
});
};
return (
<SettingsUpgradeModalContent
key={upgradeKeys.join('\n')}
account={account}
allowPermanentHide={allowPermanentHide}
dismissUpgrades={dismissUpgrades}
hideUpgrades={hideReviewUpgradeKeys}
upgradeKeys={upgradeKeys}
upgrades={settingsUpgrades}
/>
);
};
export default SettingsUpgradeModal;
@@ -133,6 +133,28 @@ describe('useBrowserPureP2PAccountUpgrade', () => {
expect(reloadMock).toHaveBeenCalledOnce(); expect(reloadMock).toHaveBeenCalledOnce();
}); });
it('upgrades imported accounts without protocol options to a reviewable router baseline', async () => {
testState.account = {
id: 'account-1',
name: 'Imported',
};
testState.accounts = [testState.account];
await renderHook();
expect(testState.setAccountMock).toHaveBeenCalledWith(
expect.objectContaining({
id: 'account-1',
pkcOptions: expect.objectContaining({
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
pkcRpcClientsOptions: undefined,
}),
}),
);
expect(reloadMock).toHaveBeenCalledOnce();
});
it('does not upgrade browser full-node accounts', async () => { it('does not upgrade browser full-node accounts', async () => {
testState.account = { testState.account = {
id: 'account-1', id: 'account-1',
@@ -2,8 +2,11 @@ import { DEFAULT_HTTP_ROUTER_URLS } from '@bitsocial/bitsocial-react-hooks/dist/
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
addBrowserHttpRoutersOptions,
configureP2PBrowserPkcOptions, configureP2PBrowserPkcOptions,
getBrowserHttpRoutersSettingsUpgrade,
getBrowserGatewayPkcOptions, getBrowserGatewayPkcOptions,
getLegacyDefaultBrowserHttpRoutersOptions,
getPureP2PBrowserPreference, getPureP2PBrowserPreference,
PURE_P2P_BROWSER_SETTING_KEY, PURE_P2P_BROWSER_SETTING_KEY,
setPureP2PBrowserPreference, setPureP2PBrowserPreference,
@@ -19,6 +22,8 @@ const createStorage = (values: Record<string, string | undefined> = {}) => ({
describe('p2p-browser-config', () => { describe('p2p-browser-config', () => {
const defaultHttpRouters = DEFAULT_HTTP_ROUTER_URLS; const defaultHttpRouters = DEFAULT_HTTP_ROUTER_URLS;
const legacyGatewayHttpRouters = ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'];
const legacyPureP2PHttpRouters = ['https://peers.plebpubsub.xyz', 'https://routing.lol', 'https://peers.pleb.bot'];
it('configures browser PKC options for pure p2p mode by default', () => { it('configures browser PKC options for pure p2p mode by default', () => {
const chainProviders = { const chainProviders = {
@@ -117,4 +122,41 @@ describe('p2p-browser-config', () => {
expect(getPureP2PBrowserPreference(targetWindow)).toBe(true); expect(getPureP2PBrowserPreference(targetWindow)).toBe(true);
expect(shouldUsePureP2PBrowser(targetWindow)).toBe(true); expect(shouldUsePureP2PBrowser(targetWindow)).toBe(true);
}); });
it('describes a reviewable upgrade for legacy shipped router defaults', () => {
expect(getLegacyDefaultBrowserHttpRoutersOptions()).toEqual(legacyGatewayHttpRouters);
expect(getBrowserHttpRoutersSettingsUpgrade(legacyGatewayHttpRouters)).toEqual({
currentHttpRoutersOptions: legacyGatewayHttpRouters,
missingDefaultHttpRoutersOptions: ['https://routerofbitsocial.xyz', 'https://bsotracker.online'],
upgradedHttpRoutersOptions: [...legacyGatewayHttpRouters, 'https://routerofbitsocial.xyz', 'https://bsotracker.online'],
});
expect(getBrowserHttpRoutersSettingsUpgrade(legacyPureP2PHttpRouters)).toEqual({
currentHttpRoutersOptions: legacyPureP2PHttpRouters,
missingDefaultHttpRoutersOptions: ['https://peers.forumindex.com', 'https://routerofbitsocial.xyz', 'https://bsotracker.online'],
upgradedHttpRoutersOptions: [...legacyPureP2PHttpRouters, 'https://peers.forumindex.com', 'https://routerofbitsocial.xyz', 'https://bsotracker.online'],
});
});
it('keeps partially applied shipped router defaults reviewable', () => {
const partiallyUpgradedRouters = [...legacyGatewayHttpRouters, 'https://routerofbitsocial.xyz'];
expect(getBrowserHttpRoutersSettingsUpgrade(partiallyUpgradedRouters)).toEqual({
currentHttpRoutersOptions: partiallyUpgradedRouters,
missingDefaultHttpRoutersOptions: ['https://bsotracker.online'],
upgradedHttpRoutersOptions: [...partiallyUpgradedRouters, 'https://bsotracker.online'],
});
});
it('does not offer default upgrades for custom router lists', () => {
const customRouters = ['https://router.custom.example', 'https://peers.pleb.bot'];
expect(getBrowserHttpRoutersSettingsUpgrade(customRouters)).toBeUndefined();
});
it('adds selected browser routers without changing existing order', () => {
expect(addBrowserHttpRoutersOptions(legacyGatewayHttpRouters, ['https://bsotracker.online', 'https://routing.lol'])).toEqual([
...legacyGatewayHttpRouters,
'https://bsotracker.online',
]);
});
}); });
+28 -1
View File
@@ -1,3 +1,4 @@
import { DEFAULT_HTTP_ROUTER_URLS } from '@bitsocial/bitsocial-react-hooks/dist/stores/accounts/account-generator.js';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
getBrowserGatewayAccountOptions, getBrowserGatewayAccountOptions,
@@ -7,6 +8,7 @@ import {
shouldShowP2PSettingsSection, shouldShowP2PSettingsSection,
shouldUpgradeBrowserPureP2PAccount, shouldUpgradeBrowserPureP2PAccount,
} from '../p2p-runtime'; } from '../p2p-runtime';
import { getLegacyDefaultBrowserHttpRoutersOptions } from '../p2p-browser-config';
const browserWindow = { const browserWindow = {
electronApi: undefined, electronApi: undefined,
@@ -94,13 +96,27 @@ describe('p2p-runtime', () => {
it('upgrades only stale gateway browser accounts when pure p2p is enabled', () => { it('upgrades only stale gateway browser accounts when pure p2p is enabled', () => {
const gatewayAccount = { pkcOptions: { ipfsGatewayUrls: ['https://gateway.example'] } }; const gatewayAccount = { pkcOptions: { ipfsGatewayUrls: ['https://gateway.example'] } };
const browserAccount = { pkcOptions: { libp2pJsClientsOptions: [{ key: 'libp2pjs' }] } }; const browserAccount = { pkcOptions: { httpRoutersOptions: DEFAULT_HTTP_ROUTER_URLS, libp2pJsClientsOptions: [{ key: 'libp2pjs' }] } };
const browserAccountWithLegacyDefaultRouters = {
pkcOptions: {
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
};
const browserAccountWithCustomRouters = {
pkcOptions: {
httpRoutersOptions: ['https://router.custom.example'],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
},
};
const mixedBrowserAccount = { pkcOptions: { libp2pJsClientsOptions: [{ key: 'libp2pjs' }], pubsubKuboRpcClientsOptions: ['https://pubsub.example/api/v0'] } }; const mixedBrowserAccount = { pkcOptions: { libp2pJsClientsOptions: [{ key: 'libp2pjs' }], pubsubKuboRpcClientsOptions: ['https://pubsub.example/api/v0'] } };
const fullNodeAccount = { pkcOptions: { pkcRpcClientsOptions: ['ws://node.example'] } }; const fullNodeAccount = { pkcOptions: { pkcRpcClientsOptions: ['ws://node.example'] } };
expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindow)).toBe(true); expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindow)).toBe(true);
expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindowWithEnabledPureP2P)).toBe(true); expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindowWithEnabledPureP2P)).toBe(true);
expect(shouldUpgradeBrowserPureP2PAccount(browserAccount, browserWindow)).toBe(false); expect(shouldUpgradeBrowserPureP2PAccount(browserAccount, browserWindow)).toBe(false);
expect(shouldUpgradeBrowserPureP2PAccount(browserAccountWithLegacyDefaultRouters, browserWindow)).toBe(false);
expect(shouldUpgradeBrowserPureP2PAccount(browserAccountWithCustomRouters, browserWindow)).toBe(false);
expect(shouldUpgradeBrowserPureP2PAccount(mixedBrowserAccount, browserWindow)).toBe(true); expect(shouldUpgradeBrowserPureP2PAccount(mixedBrowserAccount, browserWindow)).toBe(true);
expect(shouldUpgradeBrowserPureP2PAccount(fullNodeAccount, browserWindow)).toBe(false); expect(shouldUpgradeBrowserPureP2PAccount(fullNodeAccount, browserWindow)).toBe(false);
expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindowWithDisabledPureP2P)).toBe(false); expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindowWithDisabledPureP2P)).toBe(false);
@@ -117,6 +133,7 @@ describe('p2p-runtime', () => {
}; };
expect(getBrowserPureP2PAccountOptions(account)).toMatchObject({ expect(getBrowserPureP2PAccountOptions(account)).toMatchObject({
httpRoutersOptions: ['https://custom-router.example'],
libp2pJsClientsOptions: [{ key: 'libp2pjs' }], libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
ipfsGatewayUrls: undefined, ipfsGatewayUrls: undefined,
pkcRpcClientsOptions: undefined, pkcRpcClientsOptions: undefined,
@@ -130,4 +147,14 @@ describe('p2p-runtime', () => {
pubsubKuboRpcClientsOptions: ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.xyz/api/v0', 'https://rannithepleb.com/api/v0'], pubsubKuboRpcClientsOptions: ['https://pubsubprovider.xyz/api/v0', 'https://plebpubsub.xyz/api/v0', 'https://rannithepleb.com/api/v0'],
}); });
}); });
it('uses the legacy router baseline when upgrading imported accounts without saved protocol options', () => {
expect(getBrowserPureP2PAccountOptions({ id: 'imported-account' })).toMatchObject({
httpRoutersOptions: getLegacyDefaultBrowserHttpRoutersOptions(),
libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
ipfsGatewayUrls: undefined,
pkcRpcClientsOptions: undefined,
pubsubKuboRpcClientsOptions: undefined,
});
});
}); });
@@ -0,0 +1,86 @@
import { describe, expect, it } from 'vitest';
import {
applySelectedSettingsUpgrades,
getReviewableSettingsUpgrades,
getSelectedSettingsUpgradeOptionCount,
getSettingsUpgradeKey,
getSettingsUpgradeOptionSelectionKey,
type ReviewableSettingsUpgrade,
type SettingsUpgradeAccount,
} from '../settings-upgrades';
describe('settings-upgrades', () => {
it('describes HTTP tracker upgrades through the generic settings upgrade shape', () => {
const account: SettingsUpgradeAccount = {
id: 'account-1',
pkcOptions: {
httpRoutersOptions: ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'],
},
};
expect(getReviewableSettingsUpgrades(account)).toEqual([
expect.objectContaining({
id: 'http-routers',
labelKey: 'advanced_settings_upgrade_http_routers',
options: [
expect.objectContaining({ id: 'https://routerofbitsocial.xyz', label: 'https://routerofbitsocial.xyz' }),
expect.objectContaining({ id: 'https://bsotracker.online', label: 'https://bsotracker.online' }),
],
}),
]);
});
it('applies any selected setting upgrade that follows the shared contract', () => {
const account: SettingsUpgradeAccount = { id: 'account-1', existing: true };
const firstUpgrade: ReviewableSettingsUpgrade = {
id: 'first-setting',
labelKey: 'first_setting',
options: [
{ id: 'alpha', label: 'Alpha', ariaLabelKey: 'alpha' },
{ id: 'beta', label: 'Beta', ariaLabelKey: 'beta' },
],
applySelectedOptions: (nextAccount, selectedOptionIds) => ({
...nextAccount,
firstSetting: selectedOptionIds,
}),
};
const secondUpgrade: ReviewableSettingsUpgrade = {
id: 'second-setting',
labelKey: 'second_setting',
options: [{ id: 'gamma', label: 'Gamma', ariaLabelKey: 'gamma' }],
applySelectedOptions: (nextAccount, selectedOptionIds) => ({
...nextAccount,
secondSetting: selectedOptionIds,
}),
};
const selections = {
[getSettingsUpgradeOptionSelectionKey(firstUpgrade, firstUpgrade.options[0])]: true,
[getSettingsUpgradeOptionSelectionKey(firstUpgrade, firstUpgrade.options[1])]: false,
[getSettingsUpgradeOptionSelectionKey(secondUpgrade, secondUpgrade.options[0])]: true,
};
expect(getSelectedSettingsUpgradeOptionCount([firstUpgrade, secondUpgrade], selections)).toBe(2);
expect(applySelectedSettingsUpgrades(account, [firstUpgrade, secondUpgrade], selections)).toEqual({
id: 'account-1',
existing: true,
firstSetting: ['alpha'],
secondSetting: ['gamma'],
});
});
it('keys dismissed upgrades by account, upgrade id, and option signature', () => {
const account: SettingsUpgradeAccount = { id: 'account-1' };
const upgrade: ReviewableSettingsUpgrade = {
id: 'future-setting',
labelKey: 'future_setting',
options: [
{ id: 'one', label: 'One', ariaLabelKey: 'one' },
{ id: 'two', label: 'Two', ariaLabelKey: 'two' },
],
applySelectedOptions: (nextAccount) => nextAccount,
};
expect(getSettingsUpgradeKey(account, upgrade)).toBe('account-1:future-setting:one|two');
});
});
+58
View File
@@ -12,6 +12,16 @@ const DEFAULT_HTTP_ROUTER_URLS = [
'https://bsotracker.online', 'https://bsotracker.online',
]; ];
const LEGACY_DEFAULT_BROWSER_HTTP_ROUTER_URLS = ['https://routing.lol', 'https://peers.pleb.bot', 'https://peers.plebpubsub.xyz', 'https://peers.forumindex.com'];
const LEGACY_DEFAULT_HTTP_ROUTER_URL_SETS = [['https://peers.plebpubsub.xyz', 'https://routing.lol', 'https://peers.pleb.bot'], LEGACY_DEFAULT_BROWSER_HTTP_ROUTER_URLS];
export type BrowserHttpRoutersSettingsUpgrade = {
currentHttpRoutersOptions: string[];
missingDefaultHttpRoutersOptions: string[];
upgradedHttpRoutersOptions: string[];
};
export const P2P_BROWSER_PKC_OPTIONS = { export const P2P_BROWSER_PKC_OPTIONS = {
libp2pJsClientsOptions: [{ key: 'libp2pjs' }], libp2pJsClientsOptions: [{ key: 'libp2pjs' }],
ipfsGatewayUrls: undefined, ipfsGatewayUrls: undefined,
@@ -40,6 +50,54 @@ type P2PBrowserConfigWindow = {
const cloneArray = <T>(value: T[] | undefined) => (value ? [...value] : undefined); const cloneArray = <T>(value: T[] | undefined) => (value ? [...value] : undefined);
const getUniqueTrimmedUrls = (urls: string[] | undefined) =>
urls?.reduce<string[]>((uniqueUrls, url) => {
const trimmedUrl = url.trim();
if (trimmedUrl && !uniqueUrls.includes(trimmedUrl)) uniqueUrls.push(trimmedUrl);
return uniqueUrls;
}, []) ?? [];
const hasSameUrlSet = (urls: string[], comparisonUrls: string[]) => {
if (urls.length !== comparisonUrls.length) return false;
return urls.every((url) => comparisonUrls.includes(url));
};
const hasUrlSet = (urls: string[], comparisonUrls: string[]) => comparisonUrls.every((url) => urls.includes(url));
const isKnownDefaultHttpRoutersOptions = (httpRoutersOptions: string[]) => {
if (hasSameUrlSet(httpRoutersOptions, DEFAULT_HTTP_ROUTER_URLS)) return true;
if (LEGACY_DEFAULT_HTTP_ROUTER_URL_SETS.some((legacyRouters) => hasSameUrlSet(httpRoutersOptions, legacyRouters))) return true;
return (
httpRoutersOptions.every((routerUrl) => DEFAULT_HTTP_ROUTER_URLS.includes(routerUrl)) &&
LEGACY_DEFAULT_HTTP_ROUTER_URL_SETS.some((legacyRouters) => hasUrlSet(httpRoutersOptions, legacyRouters))
);
};
export const getBrowserHttpRoutersSettingsUpgrade = (httpRoutersOptions: string[] | undefined): BrowserHttpRoutersSettingsUpgrade | undefined => {
const httpRouters = getUniqueTrimmedUrls(httpRoutersOptions);
if (httpRouters.length === 0 || !isKnownDefaultHttpRoutersOptions(httpRouters)) return undefined;
const missingDefaultHttpRoutersOptions = DEFAULT_HTTP_ROUTER_URLS.filter((routerUrl) => !httpRouters.includes(routerUrl));
if (missingDefaultHttpRoutersOptions.length === 0) return undefined;
return {
currentHttpRoutersOptions: httpRouters,
missingDefaultHttpRoutersOptions,
upgradedHttpRoutersOptions: [...httpRouters, ...missingDefaultHttpRoutersOptions],
};
};
export const addBrowserHttpRoutersOptions = (httpRoutersOptions: string[] | undefined, routerUrls: string[]) => {
const normalizedRouters = getUniqueTrimmedUrls(httpRoutersOptions);
return routerUrls.reduce<string[]>((routers, routerUrl) => {
const trimmedRouterUrl = routerUrl.trim();
if (trimmedRouterUrl && !routers.includes(trimmedRouterUrl)) routers.push(trimmedRouterUrl);
return routers;
}, normalizedRouters);
};
export const getLegacyDefaultBrowserHttpRoutersOptions = () => [...LEGACY_DEFAULT_BROWSER_HTTP_ROUTER_URLS];
export const getBrowserPureP2PPkcOptions = () => ({ export const getBrowserPureP2PPkcOptions = () => ({
...P2P_BROWSER_PKC_OPTIONS, ...P2P_BROWSER_PKC_OPTIONS,
libp2pJsClientsOptions: P2P_BROWSER_PKC_OPTIONS.libp2pJsClientsOptions.map((options) => ({ ...options })), libp2pJsClientsOptions: P2P_BROWSER_PKC_OPTIONS.libp2pJsClientsOptions.map((options) => ({ ...options })),
+19 -6
View File
@@ -1,4 +1,11 @@
import { canUsePureP2PBrowser, getBrowserGatewayPkcOptions, getBrowserPureP2PPkcOptions, isElectronRuntime, shouldUsePureP2PBrowser } from './p2p-browser-config'; import {
canUsePureP2PBrowser,
getBrowserGatewayPkcOptions,
getBrowserPureP2PPkcOptions,
getLegacyDefaultBrowserHttpRoutersOptions,
isElectronRuntime,
shouldUsePureP2PBrowser,
} from './p2p-browser-config';
export const P2P_STATS_SECTION_ID = 'p2p-stats-settings'; export const P2P_STATS_SECTION_ID = 'p2p-stats-settings';
@@ -64,11 +71,17 @@ export const isBrowserPureP2PEnabled = (account?: unknown, targetWindow: Window
return shouldUsePureP2PBrowser(targetWindow); return shouldUsePureP2PBrowser(targetWindow);
}; };
export const getBrowserPureP2PAccountOptions = (account?: unknown) => ({ export const getBrowserPureP2PAccountOptions = (account?: unknown) => {
...toAccountShape(account)?.pkcOptions, const protocolOptions = toAccountShape(account)?.pkcOptions;
...getBrowserPureP2PPkcOptions(), const pureP2POptions = getBrowserPureP2PPkcOptions();
pkcRpcClientsOptions: undefined,
}); return {
...protocolOptions,
...pureP2POptions,
httpRoutersOptions: hasArrayItems(protocolOptions?.httpRoutersOptions) ? protocolOptions?.httpRoutersOptions : getLegacyDefaultBrowserHttpRoutersOptions(),
pkcRpcClientsOptions: undefined,
};
};
export const shouldUpgradeBrowserPureP2PAccount = (account?: unknown, targetWindow: Window = window) => { export const shouldUpgradeBrowserPureP2PAccount = (account?: unknown, targetWindow: Window = window) => {
if (!account || typeof account !== 'object' || !canConfigureBrowserPureP2P(targetWindow) || !isBrowserPureP2PEnabled(account, targetWindow)) return false; if (!account || typeof account !== 'object' || !canConfigureBrowserPureP2P(targetWindow) || !isBrowserPureP2PEnabled(account, targetWindow)) return false;
+84
View File
@@ -0,0 +1,84 @@
import { addBrowserHttpRoutersOptions, getBrowserHttpRoutersSettingsUpgrade } from './p2p-browser-config';
type AccountProtocolOptions = {
httpRoutersOptions?: string[];
[key: string]: unknown;
};
export type SettingsUpgradeAccount = Record<string, unknown> & {
id?: string;
name?: string;
pkcOptions?: AccountProtocolOptions;
};
export type ReviewableSettingsUpgradeOption = {
id: string;
label: string;
ariaLabelKey: string;
ariaLabelValues?: Record<string, string>;
selectedByDefault?: boolean;
};
export type ReviewableSettingsUpgrade = {
id: string;
labelKey: string;
options: ReviewableSettingsUpgradeOption[];
applySelectedOptions: (account: SettingsUpgradeAccount, selectedOptionIds: string[]) => SettingsUpgradeAccount;
};
type SettingsUpgradeDetector = (account: SettingsUpgradeAccount) => ReviewableSettingsUpgrade | undefined;
export type SettingsUpgradeSelections = Record<string, boolean>;
const getHttpRoutersSettingsUpgrade: SettingsUpgradeDetector = (account) => {
const httpRoutersUpgrade = getBrowserHttpRoutersSettingsUpgrade(account.pkcOptions?.httpRoutersOptions);
if (!httpRoutersUpgrade) return undefined;
return {
id: 'http-routers',
labelKey: 'advanced_settings_upgrade_http_routers',
options: httpRoutersUpgrade.missingDefaultHttpRoutersOptions.map((routerUrl) => ({
id: routerUrl,
label: routerUrl,
ariaLabelKey: 'advanced_settings_upgrade_http_router_aria',
ariaLabelValues: { router: routerUrl },
selectedByDefault: true,
})),
applySelectedOptions: (nextAccount, selectedRouters) => ({
...nextAccount,
pkcOptions: {
...nextAccount.pkcOptions,
httpRoutersOptions: addBrowserHttpRoutersOptions(nextAccount.pkcOptions?.httpRoutersOptions, selectedRouters),
},
}),
};
};
const SETTINGS_UPGRADE_DETECTORS: SettingsUpgradeDetector[] = [getHttpRoutersSettingsUpgrade];
export const getReviewableSettingsUpgrades = (account: SettingsUpgradeAccount) =>
SETTINGS_UPGRADE_DETECTORS.reduce<ReviewableSettingsUpgrade[]>((upgrades, getUpgrade) => {
const upgrade = getUpgrade(account);
if (upgrade) upgrades.push(upgrade);
return upgrades;
}, []);
export const getSettingsUpgradeKey = (account: SettingsUpgradeAccount, upgrade: ReviewableSettingsUpgrade) =>
`${account.id ?? account.name ?? 'unknown'}:${upgrade.id}:${upgrade.options.map((option) => option.id).join('|')}`;
export const getSettingsUpgradeOptionSelectionKey = (upgrade: ReviewableSettingsUpgrade, option: ReviewableSettingsUpgradeOption) => `${upgrade.id}:${option.id}`;
export const isSettingsUpgradeOptionSelected = (upgrade: ReviewableSettingsUpgrade, option: ReviewableSettingsUpgradeOption, selections: SettingsUpgradeSelections) =>
selections[getSettingsUpgradeOptionSelectionKey(upgrade, option)] ?? option.selectedByDefault ?? true;
export const getSelectedSettingsUpgradeOptionIds = (upgrade: ReviewableSettingsUpgrade, selections: SettingsUpgradeSelections) =>
upgrade.options.filter((option) => isSettingsUpgradeOptionSelected(upgrade, option, selections)).map((option) => option.id);
export const getSelectedSettingsUpgradeOptionCount = (upgrades: ReviewableSettingsUpgrade[], selections: SettingsUpgradeSelections) =>
upgrades.reduce((selectedCount, upgrade) => selectedCount + getSelectedSettingsUpgradeOptionIds(upgrade, selections).length, 0);
export const applySelectedSettingsUpgrades = (account: SettingsUpgradeAccount, upgrades: ReviewableSettingsUpgrade[], selections: SettingsUpgradeSelections) =>
upgrades.reduce<SettingsUpgradeAccount>((nextAccount, upgrade) => {
const selectedOptionIds = getSelectedSettingsUpgradeOptionIds(upgrade, selections);
return selectedOptionIds.length > 0 ? upgrade.applySelectedOptions(nextAccount, selectedOptionIds) : nextAccount;
}, account);
@@ -0,0 +1,67 @@
import { create } from 'zustand';
export const DISMISSED_SETTINGS_UPGRADES_KEY = '5chan:dismissed-settings-upgrades';
export const HIDDEN_SETTINGS_UPGRADE_REVIEWS_KEY = '5chan:hidden-settings-upgrade-reviews';
const uniqueUpgradeKeys = (upgradeKeys: string[]) => [...new Set(upgradeKeys)];
const readStoredUpgradeKeys = (storageKey: string) => {
try {
const parsedKeys = JSON.parse(localStorage.getItem(storageKey) ?? '[]');
return Array.isArray(parsedKeys) ? parsedKeys.filter((key): key is string => typeof key === 'string') : [];
} catch {
return [];
}
};
const writeStoredUpgradeKeys = (storageKey: string, upgradeKeys: string[]) => {
try {
localStorage.setItem(storageKey, JSON.stringify(uniqueUpgradeKeys(upgradeKeys)));
} catch {
return;
}
};
type SettingsUpgradeReviewState = {
hiddenReviewUpgradeKeys: string[];
persistentDismissedUpgradeKeys: string[];
reviewedUpgradeKeys: string[];
reviewRequestId: number;
dismissUpgradeKeys: (upgradeKeys: string[]) => void;
hideReviewUpgradeKeys: (upgradeKeys: string[]) => void;
reviewUpgradeKeys: (upgradeKeys: string[]) => void;
};
const useSettingsUpgradeReviewStore = create<SettingsUpgradeReviewState>((set, get) => ({
hiddenReviewUpgradeKeys: readStoredUpgradeKeys(HIDDEN_SETTINGS_UPGRADE_REVIEWS_KEY),
persistentDismissedUpgradeKeys: readStoredUpgradeKeys(DISMISSED_SETTINGS_UPGRADES_KEY),
reviewedUpgradeKeys: [],
reviewRequestId: 0,
dismissUpgradeKeys: (upgradeKeys) => {
const persistentDismissedUpgradeKeys = uniqueUpgradeKeys([...get().persistentDismissedUpgradeKeys, ...upgradeKeys]);
writeStoredUpgradeKeys(DISMISSED_SETTINGS_UPGRADES_KEY, persistentDismissedUpgradeKeys);
set({ persistentDismissedUpgradeKeys });
},
hideReviewUpgradeKeys: (upgradeKeys) => {
const persistentDismissedUpgradeKeys = uniqueUpgradeKeys([...get().persistentDismissedUpgradeKeys, ...upgradeKeys]);
const hiddenReviewUpgradeKeys = uniqueUpgradeKeys([...get().hiddenReviewUpgradeKeys, ...upgradeKeys]);
writeStoredUpgradeKeys(DISMISSED_SETTINGS_UPGRADES_KEY, persistentDismissedUpgradeKeys);
writeStoredUpgradeKeys(HIDDEN_SETTINGS_UPGRADE_REVIEWS_KEY, hiddenReviewUpgradeKeys);
set({ hiddenReviewUpgradeKeys, persistentDismissedUpgradeKeys });
},
reviewUpgradeKeys: (upgradeKeys) => {
const upgradeKeysToReview = new Set(upgradeKeys);
const persistentDismissedUpgradeKeys = get().persistentDismissedUpgradeKeys.filter((upgradeKey) => !upgradeKeysToReview.has(upgradeKey));
const hiddenReviewUpgradeKeys = get().hiddenReviewUpgradeKeys.filter((upgradeKey) => !upgradeKeysToReview.has(upgradeKey));
writeStoredUpgradeKeys(DISMISSED_SETTINGS_UPGRADES_KEY, persistentDismissedUpgradeKeys);
writeStoredUpgradeKeys(HIDDEN_SETTINGS_UPGRADE_REVIEWS_KEY, hiddenReviewUpgradeKeys);
set((state) => ({
hiddenReviewUpgradeKeys,
persistentDismissedUpgradeKeys,
reviewedUpgradeKeys: uniqueUpgradeKeys([...state.reviewedUpgradeKeys, ...upgradeKeys]),
reviewRequestId: state.reviewRequestId + 1,
}));
},
}));
export default useSettingsUpgradeReviewStore;