From c6d048ecd774d21b691fcd3b98ecfdc9fd3886e4 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Fri, 26 Jun 2026 17:14:31 +0700 Subject: [PATCH] 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. --- public/translations/ar/default.json | 12 +- public/translations/bn/default.json | 12 +- public/translations/cs/default.json | 12 +- public/translations/da/default.json | 12 +- public/translations/de/default.json | 12 +- public/translations/el/default.json | 12 +- public/translations/en/default.json | 12 +- public/translations/es/default.json | 12 +- public/translations/fa/default.json | 12 +- public/translations/fi/default.json | 12 +- public/translations/fil/default.json | 12 +- public/translations/fr/default.json | 12 +- public/translations/he/default.json | 12 +- public/translations/hi/default.json | 12 +- public/translations/hu/default.json | 12 +- public/translations/id/default.json | 12 +- public/translations/it/default.json | 12 +- public/translations/ja/default.json | 12 +- public/translations/ko/default.json | 12 +- public/translations/mr/default.json | 12 +- public/translations/nl/default.json | 12 +- public/translations/no/default.json | 12 +- public/translations/pl/default.json | 12 +- public/translations/pt/default.json | 12 +- public/translations/ro/default.json | 12 +- public/translations/ru/default.json | 12 +- public/translations/sq/default.json | 12 +- public/translations/sv/default.json | 12 +- public/translations/te/default.json | 12 +- public/translations/th/default.json | 12 +- public/translations/tr/default.json | 12 +- public/translations/uk/default.json | 12 +- public/translations/ur/default.json | 12 +- public/translations/vi/default.json | 12 +- public/translations/zh/default.json | 12 +- src/__tests__/app.test.tsx | 4 + src/app.tsx | 4 + .../__tests__/settings-modal.test.tsx | 76 +++++- .../__tests__/account-settings.test.tsx | 70 ++++++ .../account-settings/account-settings.tsx | 28 ++- .../__tests__/advanced-settings.test.tsx | 16 ++ .../advanced-settings/advanced-settings.tsx | 2 +- .../settings-modal/settings-modal.module.css | 36 +++ .../settings-modal/settings-modal.tsx | 47 +++- .../__tests__/settings-upgrade-modal.test.tsx | 232 ++++++++++++++++++ .../settings-upgrade-modal/index.ts | 1 + .../settings-upgrade-modal.module.css | 137 +++++++++++ .../settings-upgrade-modal.tsx | 165 +++++++++++++ ...-browser-pure-p2p-account-upgrade.test.tsx | 22 ++ src/lib/__tests__/p2p-browser-config.test.ts | 42 ++++ src/lib/__tests__/p2p-runtime.test.ts | 29 ++- src/lib/__tests__/settings-upgrades.test.ts | 86 +++++++ src/lib/p2p-browser-config.ts | 58 +++++ src/lib/p2p-runtime.ts | 25 +- src/lib/settings-upgrades.ts | 84 +++++++ .../use-settings-upgrade-review-store.ts | 67 +++++ 56 files changed, 1588 insertions(+), 63 deletions(-) create mode 100644 src/components/settings-upgrade-modal/__tests__/settings-upgrade-modal.test.tsx create mode 100644 src/components/settings-upgrade-modal/index.ts create mode 100644 src/components/settings-upgrade-modal/settings-upgrade-modal.module.css create mode 100644 src/components/settings-upgrade-modal/settings-upgrade-modal.tsx create mode 100644 src/lib/__tests__/settings-upgrades.test.ts create mode 100644 src/lib/settings-upgrades.ts create mode 100644 src/stores/use-settings-upgrade-review-store.ts diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index dc0dd01e..bf6babc7 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "وضع متصفح P2P خالص", "advanced_options_saved_reloading": "تم حفظ الخيارات، جارٍ إعادة التحميل...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "لا تظهر مرة أخرى" } diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index f4a3cf09..972f9a19 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "বিশুদ্ধ P2P ব্রাউজার মোড", "advanced_options_saved_reloading": "বিকল্পগুলো সংরক্ষণ করা হয়েছে, পুনরায় লোড করা হচ্ছে...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "আর দেখাবেন না" } diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index 6c810cd9..3dfdf58d 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -432,5 +432,15 @@ "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_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/da/default.json b/public/translations/da/default.json index ff03056b..5d15be2d 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Ren P2P-browsertilstand", "advanced_options_saved_reloading": "Indstillinger gemt, genindlæser...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/de/default.json b/public/translations/de/default.json index dffbf8dd..8a36dfc3 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Reiner P2P-Browsermodus", "advanced_options_saved_reloading": "Optionen gespeichert, wird neu geladen...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/el/default.json b/public/translations/el/default.json index 02d21b00..68e679dc 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Λειτουργία καθαρού P2P προγράμματος περιήγησης", "advanced_options_saved_reloading": "Οι επιλογές αποθηκεύτηκαν, επαναφόρτωση...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "να μην εμφανιστεί ξανά" } diff --git a/public/translations/en/default.json b/public/translations/en/default.json index 2e6f3678..be648c9e 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -451,5 +451,15 @@ "tex_preview_title": "TeX Preview", "tex_preview_protip": "Use [math][/math] tags for inline, and [eqn][/eqn] tags for block equations.", "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" } diff --git a/public/translations/es/default.json b/public/translations/es/default.json index c2079610..66333895 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Modo de navegador P2P puro", "advanced_options_saved_reloading": "Opciones guardadas, recargando...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index 6a721816..83b38487 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "حالت مرورگر P2P خالص", "advanced_options_saved_reloading": "گزینه‌ها ذخیره شد، در حال بارگذاری مجدد...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "دیگر نشان نده" } diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index 989a046f..8ec955b0 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Puhdas P2P-selaimen tila", "advanced_options_saved_reloading": "Asetukset tallennettu, ladataan uudelleen...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index 32c97abf..181ccdde 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Purong P2P browser mode", "advanced_options_saved_reloading": "Na-save ang mga opsyon, nagre-reload...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index db6f8514..0b97cbc3 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Mode navigateur P2P pur", "advanced_options_saved_reloading": "Options enregistrées, rechargement...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 3b499a23..9768a9fc 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "מצב דפדפן P2P טהור", "advanced_options_saved_reloading": "ההגדרות נשמרו, טוען מחדש...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "לא להציג שוב" } diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index e0508c82..015556a5 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "शुद्ध P2P ब्राउज़र मोड", "advanced_options_saved_reloading": "विकल्प सहेजे गए, पुनः लोड हो रहा है...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "फिर कभी न दिखाएँ" } diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index 2eeab5c3..3d0c3c7e 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -432,5 +432,15 @@ "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_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/id/default.json b/public/translations/id/default.json index cd152568..5f1f34a1 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Mode browser P2P murni", "advanced_options_saved_reloading": "Opsi disimpan, memuat ulang...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/it/default.json b/public/translations/it/default.json index aab6f034..35ca1338 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Modalità browser P2P puro", "advanced_options_saved_reloading": "Opzioni salvate, ricaricamento...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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ù" } diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index dfb24d83..5cfe6026 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "純粋なP2Pブラウザモード", "advanced_options_saved_reloading": "オプションを保存しました。再読み込み中...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "今後表示しない" } diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index 05ad5fa2..1862a70d 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "순수 P2P 브라우저 모드", "advanced_options_saved_reloading": "옵션이 저장되었습니다. 다시 불러오는 중...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "다시 표시하지 않기" } diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index 5a19daf2..85b98365 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "शुद्ध P2P ब्राउझर मोड", "advanced_options_saved_reloading": "पर्याय जतन केले, पुन्हा लोड होत आहे...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "पुन्हा दाखवू नका" } diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index e6c8e340..bf59e1f2 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Pure P2P-browsermodus", "advanced_options_saved_reloading": "Opties opgeslagen, opnieuw laden...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/no/default.json b/public/translations/no/default.json index bc7c40dc..3a913510 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Ren P2P-nettlesermodus", "advanced_options_saved_reloading": "Innstillinger lagret, laster på nytt...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index 6aa22619..0395443f 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Tryb przeglądarki czystego P2P", "advanced_options_saved_reloading": "Opcje zapisane, ładowanie ponowne...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index eb54f823..916281ce 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Modo de navegador P2P puro", "advanced_options_saved_reloading": "Opções salvas, recarregando...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index eff5359a..adfc39bf 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Mod browser P2P pur", "advanced_options_saved_reloading": "Opțiuni salvate, se reîncarcă...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index f72a7843..9d67a0e7 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Режим чистого P2P в браузере", "advanced_options_saved_reloading": "Настройки сохранены, перезагрузка...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "больше не показывать" } diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index 5fa66d5e..c1fc1359 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Modalitet shfletuesi P2P i pastër", "advanced_options_saved_reloading": "Opsionet u ruajtën, po ringarkohet...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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ë" } diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index a891e0b4..c26234df 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Rent P2P-webbläsarläge", "advanced_options_saved_reloading": "Alternativ sparade, laddar om...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/te/default.json b/public/translations/te/default.json index cd21e46f..9b1a84ff 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "శుద్ధ P2P బ్రౌజర్ మోడ్", "advanced_options_saved_reloading": "ఎంపికలు సేవ్ చేయబడ్డాయి, మళ్లీ లోడ్ అవుతోంది...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "మళ్లీ చూపించవద్దు" } diff --git a/public/translations/th/default.json b/public/translations/th/default.json index b4869e9e..79f6a5e9 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "โหมดเบราว์เซอร์ P2P แบบบริสุทธิ์", "advanced_options_saved_reloading": "บันทึกตัวเลือกแล้ว กำลังโหลดใหม่...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "ไม่ต้องแสดงอีก" } diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index 46b0873b..866c83d6 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Saf P2P tarayıcı modu", "advanced_options_saved_reloading": "Seçenekler kaydedildi, yeniden yükleniyor...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index 62690016..849e93ba 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "Режим чистого P2P у браузері", "advanced_options_saved_reloading": "Параметри збережено, перезавантаження...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "більше не показувати" } diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index fa99710a..a277d1e7 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "خالص P2P براؤزر موڈ", "advanced_options_saved_reloading": "اختیارات محفوظ ہو گئیں، دوبارہ لوڈ ہو رہا ہے...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "دوبارہ نہ دکھائیں" } diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index a7c5a863..ef1493b1 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -432,5 +432,15 @@ "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_p2p_rpc_placeholder": "ws://:/", - "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" } diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 841f61c2..8c6ea9a9 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -432,5 +432,15 @@ "advanced_pure_p2p_browser_aria": "纯 P2P 浏览器模式", "advanced_options_saved_reloading": "选项已保存,正在重新加载...", "advanced_p2p_rpc_placeholder": "ws://:/", - "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": "不再显示" } diff --git a/src/__tests__/app.test.tsx b/src/__tests__/app.test.tsx index 7a02a53d..43b54d29 100644 --- a/src/__tests__/app.test.tsx +++ b/src/__tests__/app.test.tsx @@ -270,6 +270,10 @@ vi.mock('../components/settings-modal', () => ({ default: MockSettingsModal, })); +vi.mock('../components/settings-upgrade-modal', () => ({ + default: makeNamedComponent('settings-upgrade-modal'), +})); + vi.mock('../components/reply-modal', () => ({ default: ({ parentCid, postCid }: { parentCid: string; postCid: string }) => createElement('div', { 'data-testid': 'reply-modal' }, `${parentCid}:${postCid}`), })); diff --git a/src/app.tsx b/src/app.tsx index d1a6b19b..4a399ea6 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -63,6 +63,7 @@ const DirectoryModal = lazy(() => import('./components/directory-modal')); const DisclaimerModal = lazy(() => import('./components/disclaimer-modal')); const ReplyModal = lazy(() => import('./components/reply-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 // to prevent visible loading delays when switching themes @@ -225,6 +226,9 @@ const GlobalLayout = () => { + + + {activeCid && threadCid && activeCommunityAddress && ( void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; const testState = vi.hoisted(() => ({ account: { + id: 'account-1', pkcOptions: { libp2pJsClientsOptions: [{ key: 'libp2pjs' }], }, } as Record, + rpcSettings: { + state: 'disconnected', + } as Record, })); vi.mock('@bitsocial/bitsocial-react-hooks', () => ({ useAccount: () => testState.account, + usePkcRpcSettings: () => testState.rpcSettings, })); vi.mock('react-i18next', () => ({ @@ -26,39 +32,39 @@ vi.mock('react-i18next', () => ({ }), })); -vi.mock('../account-settings', () => ({ +vi.mock('../account-settings/account-settings', () => ({ default: () =>
account-settings
, })); -vi.mock('../crypto-address-setting', () => ({ +vi.mock('../crypto-address-setting/crypto-address-setting', () => ({ default: () =>
crypto-address-setting
, })); -vi.mock('../crypto-wallets-setting', () => ({ +vi.mock('../crypto-wallets-setting/crypto-wallets-setting', () => ({ default: () =>
crypto-wallets-setting
, })); -vi.mock('../interface-settings', () => ({ +vi.mock('../interface-settings/interface-settings', () => ({ default: () =>
interface-settings
, })); -vi.mock('../media-hosting-settings', () => ({ +vi.mock('../media-hosting-settings/media-hosting-settings', () => ({ default: () =>
media-hosting-settings
, })); -vi.mock('../advanced-settings', () => ({ +vi.mock('../advanced-settings/advanced-settings', () => ({ default: () =>
advanced-settings
, })); -vi.mock('../subscriptions-setting', () => ({ +vi.mock('../subscriptions-setting/subscriptions-setting', () => ({ default: () =>
subscriptions-settings
, })); -vi.mock('../trusted-board-links-setting', () => ({ +vi.mock('../trusted-board-links-setting/trusted-board-links-setting', () => ({ default: () =>
trusted-board-links-settings
, })); -vi.mock('../p2p-stats-settings', () => ({ +vi.mock('../p2p-stats-settings/p2p-stats-settings', () => ({ default: () =>
p2p-stats-settings
, })); @@ -91,6 +97,22 @@ const getSectionToggleByText = (text: string) => { describe('SettingsModal', () => { beforeEach(() => { 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'); document.body.appendChild(container); root = createRoot(container); @@ -175,6 +197,42 @@ describe('SettingsModal', () => { 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', () => { render('/all/settings#p2p-stats-settings'); diff --git a/src/components/settings-modal/account-settings/__tests__/account-settings.test.tsx b/src/components/settings-modal/account-settings/__tests__/account-settings.test.tsx index 4a870409..58885e14 100644 --- a/src/components/settings-modal/account-settings/__tests__/account-settings.test.tsx +++ b/src/components/settings-modal/account-settings/__tests__/account-settings.test.tsx @@ -8,6 +8,7 @@ import AccountSettings from '../account-settings'; (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; const act = (React as { act?: (cb: () => void | Promise) => void | Promise }).act as (cb: () => void | Promise) => void | Promise; 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(() => ({ deleteAccount: vi.fn(), @@ -343,6 +344,7 @@ describe('AccountSettings', () => { expect(hookMocks.importAccount).toHaveBeenCalledOnce(); const importedPayload = JSON.parse(hookMocks.importAccount.mock.calls[0][0]); 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('importedAccountAddress')).toBe('0x999'); expect(hookMocks.setActiveAccount).toHaveBeenCalledWith('Imported'); @@ -350,6 +352,74 @@ describe('AccountSettings', () => { 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 () => { hookMocks.useAccounts.mockReturnValue({ accounts: [ diff --git a/src/components/settings-modal/account-settings/account-settings.tsx b/src/components/settings-modal/account-settings/account-settings.tsx index 054983b3..dce10155 100644 --- a/src/components/settings-modal/account-settings/account-settings.tsx +++ b/src/components/settings-modal/account-settings/account-settings.tsx @@ -4,6 +4,7 @@ import { deleteAccount, exportAccount, importAccount, setActiveAccount, useAccou import styles from './account-settings.module.css'; import { Capacitor } from '@capacitor/core'; import { useLocation, useNavigate } from 'react-router-dom'; +import { getLegacyDefaultBrowserHttpRoutersOptions } from '../../../lib/p2p-browser-config'; const isAndroid = Capacitor.getPlatform() === 'android'; 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; }; +type ImportedAccountProtocolOptions = Record & { + 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 const AccountSettingsEditor = ({ account, @@ -139,7 +152,13 @@ const AccountSettingsEditor = ({ } const accountData = safeParseJSON<{ - account?: { communities?: Record; subscriptions?: string[]; author?: { address?: string }; name?: string }; + account?: { + communities?: Record; + subscriptions?: string[]; + author?: { address?: string }; + name?: string; + pkcOptions?: ImportedAccountProtocolOptions; + }; }>(fileContent); if (!accountData) { alert('Invalid JSON in file.'); @@ -162,6 +181,13 @@ const AccountSettingsEditor = ({ 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 importedAccountActiveName = getImportedAccountActiveName(accountData.account?.name, accounts); const result = await withErrorHandling( diff --git a/src/components/settings-modal/advanced-settings/__tests__/advanced-settings.test.tsx b/src/components/settings-modal/advanced-settings/__tests__/advanced-settings.test.tsx index b6074f21..8508797b 100644 --- a/src/components/settings-modal/advanced-settings/__tests__/advanced-settings.test.tsx +++ b/src/components/settings-modal/advanced-settings/__tests__/advanced-settings.test.tsx @@ -220,6 +220,22 @@ describe('AdvancedSettings', () => { 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 () => { localStorage.setItem('5chan:pure-p2p-browser-enabled', 'false'); diff --git a/src/components/settings-modal/advanced-settings/advanced-settings.tsx b/src/components/settings-modal/advanced-settings/advanced-settings.tsx index fa435002..b277edbf 100644 --- a/src/components/settings-modal/advanced-settings/advanced-settings.tsx +++ b/src/components/settings-modal/advanced-settings/advanced-settings.tsx @@ -293,7 +293,7 @@ const AdvancedSettings = () => { 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 dataPath = p2pDataPathRef.current?.value.trim() || undefined; diff --git a/src/components/settings-modal/settings-modal.module.css b/src/components/settings-modal/settings-modal.module.css index 1be5d64a..562a85c0 100644 --- a/src/components/settings-modal/settings-modal.module.css +++ b/src/components/settings-modal/settings-modal.module.css @@ -99,6 +99,24 @@ 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 { appearance: none; background: transparent; @@ -109,11 +127,29 @@ 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 { color: var(--button-desktop-text-color-hover); cursor: pointer; } +.settingsUpgradeNotice button:hover { + color: inherit; + cursor: pointer; +} + .setting { margin-bottom: 8px; text-transform: capitalize; diff --git a/src/components/settings-modal/settings-modal.tsx b/src/components/settings-modal/settings-modal.tsx index f8437ede..da452996 100644 --- a/src/components/settings-modal/settings-modal.tsx +++ b/src/components/settings-modal/settings-modal.tsx @@ -1,18 +1,20 @@ 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 { useTranslation } from 'react-i18next'; import styles from './settings-modal.module.css'; -import AccountSettings from './account-settings'; -import CryptoAddressSetting from './crypto-address-setting'; -import CryptoWalletsSetting from './crypto-wallets-setting'; -import InterfaceSettings from './interface-settings'; -import MediaHostingSettings from './media-hosting-settings'; -import AdvancedSettings from './advanced-settings'; -import SubscriptionsSetting from './subscriptions-setting'; -import TrustedBoardLinksSetting from './trusted-board-links-setting'; -import P2PStatsSettings from './p2p-stats-settings'; +import AccountSettings from './account-settings/account-settings'; +import CryptoAddressSetting from './crypto-address-setting/crypto-address-setting'; +import CryptoWalletsSetting from './crypto-wallets-setting/crypto-wallets-setting'; +import InterfaceSettings from './interface-settings/interface-settings'; +import MediaHostingSettings from './media-hosting-settings/media-hosting-settings'; +import AdvancedSettings from './advanced-settings/advanced-settings'; +import SubscriptionsSetting from './subscriptions-setting/subscriptions-setting'; +import TrustedBoardLinksSetting from './trusted-board-links-setting/trusted-board-links-setting'; +import P2PStatsSettings from './p2p-stats-settings/p2p-stats-settings'; 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 = [ 'interface-settings', @@ -32,11 +34,22 @@ const hashToSection = (hash: string, sectionIds = allSectionIds): string | null const SettingsModal = () => { const { t } = useTranslation(); const account = useAccount(); + const pkcRpc = usePkcRpcSettings(); const { hash: locationHash, pathname } = useLocation(); const navigate = useNavigate(); + const hiddenReviewUpgradeKeys = useSettingsUpgradeReviewStore((state) => state.hiddenReviewUpgradeKeys); + const reviewUpgradeKeys = useSettingsUpgradeReviewStore((state) => state.reviewUpgradeKeys); const hash = locationHash.slice(1); const sectionIds = useMemo(() => (shouldShowP2PSettingsSection(account) ? [...allSectionIds, P2P_STATS_SECTION_ID] : allSectionIds), [account]); 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 newPath = pathname.replace(/\/settings$/, ''); @@ -109,6 +122,10 @@ const SettingsModal = () => { } }; + const handleReviewSettingsUpgrades = () => { + reviewUpgradeKeys(visibleSettingsUpgradeKeys); + }; + const handleKeyDown = (handler: () => void) => (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); @@ -202,6 +219,16 @@ const SettingsModal = () => { {showP2PStatsSettings && } )} + {showSettingsUpgradeReview && ( + + {t('settings_upgrade_review_notice')} + {' ['} + + {']'} + + )} ); diff --git a/src/components/settings-upgrade-modal/__tests__/settings-upgrade-modal.test.tsx b/src/components/settings-upgrade-modal/__tests__/settings-upgrade-modal.test.tsx new file mode 100644 index 00000000..1caae3f1 --- /dev/null +++ b/src/components/settings-upgrade-modal/__tests__/settings-upgrade-modal.test.tsx @@ -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 | Promise }).act as (cb: () => void | Promise) => void | Promise; + +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 | undefined, + rpcSettings: { + state: 'disconnected', + } as Record, + 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; +let consoleLogSpy: ReturnType; +let container: HTMLDivElement; +const originalLocation = window.location; +let reloadMock: ReturnType; +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('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(); + }); +}); diff --git a/src/components/settings-upgrade-modal/index.ts b/src/components/settings-upgrade-modal/index.ts new file mode 100644 index 00000000..cb06731c --- /dev/null +++ b/src/components/settings-upgrade-modal/index.ts @@ -0,0 +1 @@ +export { default } from './settings-upgrade-modal'; diff --git a/src/components/settings-upgrade-modal/settings-upgrade-modal.module.css b/src/components/settings-upgrade-modal/settings-upgrade-modal.module.css new file mode 100644 index 00000000..c3809a9c --- /dev/null +++ b/src/components/settings-upgrade-modal/settings-upgrade-modal.module.css @@ -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); + } +} diff --git a/src/components/settings-upgrade-modal/settings-upgrade-modal.tsx b/src/components/settings-upgrade-modal/settings-upgrade-modal.tsx new file mode 100644 index 00000000..39238eda --- /dev/null +++ b/src/components/settings-upgrade-modal/settings-upgrade-modal.tsx @@ -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({}); + 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 ( +
+
+
+

{t('advanced_settings_upgrade_tip')}

+ {upgrades.map((upgrade) => ( +
+ {t(upgrade.labelKey)} + {upgrade.options.map((option) => ( + + ))} +
+ ))} +
+ + +
+ {allowPermanentHide && ( +
+ +
+ )} +
+ + + ); +}; + +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 ( + + ); +}; + +export default SettingsUpgradeModal; diff --git a/src/hooks/__tests__/use-browser-pure-p2p-account-upgrade.test.tsx b/src/hooks/__tests__/use-browser-pure-p2p-account-upgrade.test.tsx index cba89ece..150ce661 100644 --- a/src/hooks/__tests__/use-browser-pure-p2p-account-upgrade.test.tsx +++ b/src/hooks/__tests__/use-browser-pure-p2p-account-upgrade.test.tsx @@ -133,6 +133,28 @@ describe('useBrowserPureP2PAccountUpgrade', () => { 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 () => { testState.account = { id: 'account-1', diff --git a/src/lib/__tests__/p2p-browser-config.test.ts b/src/lib/__tests__/p2p-browser-config.test.ts index 65dbb7db..d2523980 100644 --- a/src/lib/__tests__/p2p-browser-config.test.ts +++ b/src/lib/__tests__/p2p-browser-config.test.ts @@ -2,8 +2,11 @@ import { DEFAULT_HTTP_ROUTER_URLS } from '@bitsocial/bitsocial-react-hooks/dist/ import { describe, expect, it } from 'vitest'; import { + addBrowserHttpRoutersOptions, configureP2PBrowserPkcOptions, + getBrowserHttpRoutersSettingsUpgrade, getBrowserGatewayPkcOptions, + getLegacyDefaultBrowserHttpRoutersOptions, getPureP2PBrowserPreference, PURE_P2P_BROWSER_SETTING_KEY, setPureP2PBrowserPreference, @@ -19,6 +22,8 @@ const createStorage = (values: Record = {}) => ({ describe('p2p-browser-config', () => { 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', () => { const chainProviders = { @@ -117,4 +122,41 @@ describe('p2p-browser-config', () => { expect(getPureP2PBrowserPreference(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', + ]); + }); }); diff --git a/src/lib/__tests__/p2p-runtime.test.ts b/src/lib/__tests__/p2p-runtime.test.ts index 0ed5c5d9..aa691ced 100644 --- a/src/lib/__tests__/p2p-runtime.test.ts +++ b/src/lib/__tests__/p2p-runtime.test.ts @@ -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 { getBrowserGatewayAccountOptions, @@ -7,6 +8,7 @@ import { shouldShowP2PSettingsSection, shouldUpgradeBrowserPureP2PAccount, } from '../p2p-runtime'; +import { getLegacyDefaultBrowserHttpRoutersOptions } from '../p2p-browser-config'; const browserWindow = { electronApi: undefined, @@ -94,13 +96,27 @@ describe('p2p-runtime', () => { it('upgrades only stale gateway browser accounts when pure p2p is enabled', () => { 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 fullNodeAccount = { pkcOptions: { pkcRpcClientsOptions: ['ws://node.example'] } }; expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindow)).toBe(true); expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindowWithEnabledPureP2P)).toBe(true); 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(fullNodeAccount, browserWindow)).toBe(false); expect(shouldUpgradeBrowserPureP2PAccount(gatewayAccount, browserWindowWithDisabledPureP2P)).toBe(false); @@ -117,6 +133,7 @@ describe('p2p-runtime', () => { }; expect(getBrowserPureP2PAccountOptions(account)).toMatchObject({ + httpRoutersOptions: ['https://custom-router.example'], libp2pJsClientsOptions: [{ key: 'libp2pjs' }], ipfsGatewayUrls: 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'], }); }); + + 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, + }); + }); }); diff --git a/src/lib/__tests__/settings-upgrades.test.ts b/src/lib/__tests__/settings-upgrades.test.ts new file mode 100644 index 00000000..3f9d6561 --- /dev/null +++ b/src/lib/__tests__/settings-upgrades.test.ts @@ -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'); + }); +}); diff --git a/src/lib/p2p-browser-config.ts b/src/lib/p2p-browser-config.ts index 2a7a1389..b58090f2 100644 --- a/src/lib/p2p-browser-config.ts +++ b/src/lib/p2p-browser-config.ts @@ -12,6 +12,16 @@ const DEFAULT_HTTP_ROUTER_URLS = [ '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 = { libp2pJsClientsOptions: [{ key: 'libp2pjs' }], ipfsGatewayUrls: undefined, @@ -40,6 +50,54 @@ type P2PBrowserConfigWindow = { const cloneArray = (value: T[] | undefined) => (value ? [...value] : undefined); +const getUniqueTrimmedUrls = (urls: string[] | undefined) => + urls?.reduce((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((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 = () => ({ ...P2P_BROWSER_PKC_OPTIONS, libp2pJsClientsOptions: P2P_BROWSER_PKC_OPTIONS.libp2pJsClientsOptions.map((options) => ({ ...options })), diff --git a/src/lib/p2p-runtime.ts b/src/lib/p2p-runtime.ts index 140d8308..74779f76 100644 --- a/src/lib/p2p-runtime.ts +++ b/src/lib/p2p-runtime.ts @@ -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'; @@ -64,11 +71,17 @@ export const isBrowserPureP2PEnabled = (account?: unknown, targetWindow: Window return shouldUsePureP2PBrowser(targetWindow); }; -export const getBrowserPureP2PAccountOptions = (account?: unknown) => ({ - ...toAccountShape(account)?.pkcOptions, - ...getBrowserPureP2PPkcOptions(), - pkcRpcClientsOptions: undefined, -}); +export const getBrowserPureP2PAccountOptions = (account?: unknown) => { + const protocolOptions = toAccountShape(account)?.pkcOptions; + const pureP2POptions = getBrowserPureP2PPkcOptions(); + + return { + ...protocolOptions, + ...pureP2POptions, + httpRoutersOptions: hasArrayItems(protocolOptions?.httpRoutersOptions) ? protocolOptions?.httpRoutersOptions : getLegacyDefaultBrowserHttpRoutersOptions(), + pkcRpcClientsOptions: undefined, + }; +}; export const shouldUpgradeBrowserPureP2PAccount = (account?: unknown, targetWindow: Window = window) => { if (!account || typeof account !== 'object' || !canConfigureBrowserPureP2P(targetWindow) || !isBrowserPureP2PEnabled(account, targetWindow)) return false; diff --git a/src/lib/settings-upgrades.ts b/src/lib/settings-upgrades.ts new file mode 100644 index 00000000..fdd3df58 --- /dev/null +++ b/src/lib/settings-upgrades.ts @@ -0,0 +1,84 @@ +import { addBrowserHttpRoutersOptions, getBrowserHttpRoutersSettingsUpgrade } from './p2p-browser-config'; + +type AccountProtocolOptions = { + httpRoutersOptions?: string[]; + [key: string]: unknown; +}; + +export type SettingsUpgradeAccount = Record & { + id?: string; + name?: string; + pkcOptions?: AccountProtocolOptions; +}; + +export type ReviewableSettingsUpgradeOption = { + id: string; + label: string; + ariaLabelKey: string; + ariaLabelValues?: Record; + 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; + +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((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((nextAccount, upgrade) => { + const selectedOptionIds = getSelectedSettingsUpgradeOptionIds(upgrade, selections); + return selectedOptionIds.length > 0 ? upgrade.applySelectedOptions(nextAccount, selectedOptionIds) : nextAccount; + }, account); diff --git a/src/stores/use-settings-upgrade-review-store.ts b/src/stores/use-settings-upgrade-review-store.ts new file mode 100644 index 00000000..9823a5e1 --- /dev/null +++ b/src/stores/use-settings-upgrade-review-store.ts @@ -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((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;