feat(settings): move account json editing to dedicated full editor route

This commit is contained in:
plebeius
2026-02-24 20:33:07 +08:00
parent dc43a8438e
commit fb9443a0f8
47 changed files with 852 additions and 219 deletions
+2
View File
@@ -18,6 +18,7 @@
"@types/react-dom": "18.2.10",
"@use-gesture/react": "10.3.1",
"@vercel/analytics": "^1.6.1",
"ace-builds": "1.41.0",
"cross-env": "7.0.3",
"electron-context-menu": "3.3.0",
"electron-is-dev": "2.0.0",
@@ -32,6 +33,7 @@
"lodash": "4.17.23",
"memoizee": "0.4.15",
"react": "19.1.2",
"react-ace": "14.0.1",
"react-dom": "19.1.2",
"react-i18next": "15.7.4",
"react-markdown": "8.0.6",
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "أعلى",
"reply_count": "عدد الردود",
"pagination.pageCurrent": "الصفحة {{page}} (الحالية)",
"pagination.pageLabel": "صفحة"
"pagination.pageLabel": "صفحة",
"archive": "الأرشيف",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "تحذير المفتاح الخاص",
"private_key_warning_description": "يعرض هذا المحرر المفتاح الخاص لحسابك. لا تشارك هذه البيانات مع أي شخص. التحرير بشكل خاطئ قد يكسر حسابك.",
"go_back": "رجوع",
"continue": "متابعة",
"loading_editor": "جاري تحميل المحرر...",
"editor_fallback_warning": "فشل تحميل محرر الأكواد. يتم استخدام حقل نص عادي بدلاً منه.",
"return_to_settings": "العودة إلى الإعدادات"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "শীর্ষ",
"reply_count": "উত্তরসংখ্যা",
"pagination.pageCurrent": "পৃষ্ঠা {{page}} (বর্তমান)",
"pagination.pageLabel": "পৃষ্ঠা"
"pagination.pageLabel": "পৃষ্ঠা",
"archive": "সংরক্ষণাগার",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "প্রাইভেট কী সতর্কতা",
"private_key_warning_description": "এই সম্পাদক আপনার অ্যাকাউন্টের প্রাইভেট কী দেখায়। এই ডেটা কারও সাথে শেয়ার করবেন না। ভুলভাবে সম্পাদনা করলে আপনার অ্যাকাউন্ট ক্ষতিগ্রস্ত হতে পারে।",
"go_back": "ফিরে যান",
"continue": "চালিয়ে যান",
"loading_editor": "এডিটর লোড হচ্ছে...",
"editor_fallback_warning": "কোড এডিটর লোড করতে ব্যর্থ হয়েছে। পরিবর্তে একটি সাধারণ টেক্সট এরিয়া ব্যবহার করা হচ্ছে।",
"return_to_settings": "সেটিংসে ফিরে যান"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Nahoru",
"reply_count": "Počet odpovědí",
"pagination.pageCurrent": "Stránka {{page}} (aktuální)",
"pagination.pageLabel": "Stránka"
"pagination.pageLabel": "Stránka",
"archive": "Archiv",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Varování o privátním klíči",
"private_key_warning_description": "Tento editor zobrazuje soukromý klíč vašeho účtu. Tyto údaje nikomu nesdílejte. Nesprávná úprava může váš účet poškodit.",
"go_back": "Zpět",
"continue": "Pokračovat",
"loading_editor": "Načítání editoru...",
"editor_fallback_warning": "Načtení editoru kódu selhalo. Místo toho se používá jednoduché textové pole.",
"return_to_settings": "Zpět na nastavení"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Top",
"reply_count": "Antal svar",
"pagination.pageCurrent": "Side {{page}} (nuværende)",
"pagination.pageLabel": "Side"
"pagination.pageLabel": "Side",
"archive": "Arkiv",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Advarsel om privat nøgle",
"private_key_warning_description": "Denne editor viser din kontos private nøgle. Del ikke disse data med nogen. Forkert redigering kan ødelægge din konto.",
"go_back": "Gå tilbage",
"continue": "Fortsæt",
"loading_editor": "Indlæser editor...",
"editor_fallback_warning": "Kodeeditoren kunne ikke indlæses. Bruger et simpelt tekstfelt i stedet.",
"return_to_settings": "Tilbage til indstillinger"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Oben",
"reply_count": "Anzahl Antworten",
"pagination.pageCurrent": "Seite {{page}} (aktuell)",
"pagination.pageLabel": "Seite"
"pagination.pageLabel": "Seite",
"archive": "Archiv",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Warnung zum privaten Schlüssel",
"private_key_warning_description": "Dieser Editor zeigt den privaten Schlüssel Ihres Kontos an. Teilen Sie diese Daten mit niemandem. Falsche Bearbeitung kann Ihr Konto beschädigen.",
"go_back": "Zurück",
"continue": "Weiter",
"loading_editor": "Editor wird geladen...",
"editor_fallback_warning": "Der Code-Editor konnte nicht geladen werden. Es wird stattdessen ein einfaches Textfeld verwendet.",
"return_to_settings": "Zurück zu den Einstellungen"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Πάνω",
"reply_count": "Αριθμός απαντήσεων",
"pagination.pageCurrent": "Σελίδα {{page}} (τρέχουσα)",
"pagination.pageLabel": "Σελίδα"
"pagination.pageLabel": "Σελίδα",
"archive": "Αρχείο",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Προειδοποίηση ιδιωτικού κλειδιού",
"private_key_warning_description": "Αυτό το πρόγραμμα επεξεργασίας εμφανίζει το ιδιωτικό κλειδί του λογαριασμού σας. Μην μοιράζεστε αυτά τα δεδομένα με κανέναν. Η λανθασμένη επεξεργασία μπορεί να καταστρέψει τον λογαριασμό σας.",
"go_back": "Επιστροφή",
"continue": "Συνέχεια",
"loading_editor": "Φόρτωση του επεξεργαστή...",
"editor_fallback_warning": "Αποτυχία φόρτωσης του επεξεργαστή κώδικα. Χρησιμοποιείται απλό πεδίο κειμένου αντί για αυτό.",
"return_to_settings": "Επιστροφή στις Ρυθμίσεις"
}
+8 -1
View File
@@ -283,5 +283,12 @@
"show_all": "Show All",
"show_blotter": "Show Blotter",
"reply_count": "Reply count",
"pagination.pageLabel": "Page"
"pagination.pageLabel": "Page",
"private_key_warning_title": "Private Key Warning",
"private_key_warning_description": "This editor shows your account's private key. Do not share this data with anyone. Editing incorrectly may break your account.",
"go_back": "Go Back",
"continue": "Continue",
"loading_editor": "Loading editor...",
"editor_fallback_warning": "The code editor failed to load. Using a plain textarea instead.",
"return_to_settings": "Return to Settings"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Arriba",
"reply_count": "Número de respuestas",
"pagination.pageCurrent": "Página {{page}} (actual)",
"pagination.pageLabel": "Página"
"pagination.pageLabel": "Página",
"archive": "Archivo",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Advertencia de clave privada",
"private_key_warning_description": "Este editor muestra la clave privada de tu cuenta. No compartas estos datos con nadie. Editar incorrectamente puede dañar tu cuenta.",
"go_back": "Volver",
"continue": "Continuar",
"loading_editor": "Cargando editor...",
"editor_fallback_warning": "No se pudo cargar el editor de código. Se usa un campo de texto simple en su lugar.",
"return_to_settings": "Volver a Configuración"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "بالا",
"reply_count": "تعداد پاسخ‌ها",
"pagination.pageCurrent": "صفحه {{page}} (فعلی)",
"pagination.pageLabel": "صفحه"
"pagination.pageLabel": "صفحه",
"archive": "بایگانی",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "هشدار کلید خصوصی",
"private_key_warning_description": "این ویرایشگر کلید خصوصی حساب شما را نمایش می‌دهد. این داده‌ها را با هیچ‌کس به اشتراک نگذارید. ویرایش نادرست ممکن است حساب شما را خراب کند.",
"go_back": "بازگشت",
"continue": "ادامه",
"loading_editor": "در حال بارگذاری ویرایشگر...",
"editor_fallback_warning": "بارگذاری ویرایشگر کد ناموفق بود. در عوض از یک فیلد متنی ساده استفاده می‌شود.",
"return_to_settings": "بازگشت به تنظیمات"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Ylös",
"reply_count": "Vastausten määrä",
"pagination.pageCurrent": "Sivu {{page}} (nykyinen)",
"pagination.pageLabel": "Sivu"
"pagination.pageLabel": "Sivu",
"archive": "Arkisto",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Yksityisen avaimen varoitus",
"private_key_warning_description": "Tämä editori näyttää tilisi yksityisen avaimen. Älä jaa näitä tietoja kenenkään kanssa. Virheellinen muokkaus voi rikkoa tilisi.",
"go_back": "Takaisin",
"continue": "Jatka",
"loading_editor": "Ladataan editoria...",
"editor_fallback_warning": "Koodieditorin lataus epäonnistui. Käytetään sen sijaan yksinkertaista tekstikenttää.",
"return_to_settings": "Palaa asetuksiin"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Itaas",
"reply_count": "Bilang ng mga tugon",
"pagination.pageCurrent": "Pahina {{page}} (kasalukuyan)",
"pagination.pageLabel": "Pahina"
"pagination.pageLabel": "Pahina",
"archive": "Arkibo",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Babala sa Pribadong Susi",
"private_key_warning_description": "Ang editor na ito ay nagpapakita ng pribadong susi ng iyong account. Huwag ibahagi ang data na ito sa sinuman. Ang maling pag-edit ay maaaring masira ang iyong account.",
"go_back": "Bumalik",
"continue": "Magpatuloy",
"loading_editor": "Nilo-load ang editor...",
"editor_fallback_warning": "Nabigo ang pag-load ng code editor. Gumagamit ng plain textarea sa halip.",
"return_to_settings": "Bumalik sa Mga Setting"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Haut",
"reply_count": "Nombre de réponses",
"pagination.pageCurrent": "Page {{page}} (actuelle)",
"pagination.pageLabel": "Page"
"pagination.pageLabel": "Page",
"archive": "Archives",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Avertissement concernant la clé privée",
"private_key_warning_description": "Cet éditeur affiche la clé privée de votre compte. Ne partagez pas ces données avec qui que ce soit. Une modification incorrecte peut endommager votre compte.",
"go_back": "Retour",
"continue": "Continuer",
"loading_editor": "Chargement de l'éditeur...",
"editor_fallback_warning": "Le chargement de l'éditeur de code a échoué. Utilisation d'un champ de texte simple à la place.",
"return_to_settings": "Retour aux paramètres"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "למעלה",
"reply_count": "מספר תגובות",
"pagination.pageCurrent": "עמוד {{page}} (נוכחי)",
"pagination.pageLabel": "עמוד"
"pagination.pageLabel": "עמוד",
"archive": "ארכיון",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "אזהרת מפתח פרטי",
"private_key_warning_description": "עורך זה מציג את המפתח הפרטי של חשבונך. אל תשתף נתונים אלה עם אף אחד. עריכה שגויה עלולה לשבור את חשבונך.",
"go_back": "חזור",
"continue": "המשך",
"loading_editor": "טוען עורך...",
"editor_fallback_warning": "טעינת עורך הקוד נכשלה. משתמשים בשדה טקסט פשוט במקום.",
"return_to_settings": "חזרה להגדרות"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "शीर्ष",
"reply_count": "जवाबों की संख्या",
"pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)",
"pagination.pageLabel": "पृष्ठ"
"pagination.pageLabel": "पृष्ठ",
"archive": "संग्रह",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "निजी कुंजी चेतावनी",
"private_key_warning_description": "यह संपादक आपके खाते की निजी कुंजी दिखाता है। इस डेटा को किसी के साथ साझा न करें। गलत तरीके से संपादन करने से आपका खाता खराब हो सकता है।",
"go_back": "वापस जाएं",
"continue": "जारी रखें",
"loading_editor": "संपादक लोड हो रहा है...",
"editor_fallback_warning": "कोड एडिटर लोड नहीं हो सका। इसके बजाय एक सामान्य टेक्स्ट एरिया का उपयोग किया जा रहा है।",
"return_to_settings": "सेटिंग्स पर वापस जाएं"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Felül",
"reply_count": "Válaszok száma",
"pagination.pageCurrent": "{{page}}. oldal (jelenlegi)",
"pagination.pageLabel": "Oldal"
"pagination.pageLabel": "Oldal",
"archive": "Archívum",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Privát kulcs figyelmeztetés",
"private_key_warning_description": "Ez a szerkesztő megjeleníti fiókod titkos kulcsát. Ne osszd meg ezeket az adatokat senkivel. A helytelen szerkesztés tönkreteheti a fiókodat.",
"go_back": "Vissza",
"continue": "Folytatás",
"loading_editor": "Szerkesztő betöltése...",
"editor_fallback_warning": "A kódszerkesztő betöltése sikertelen. Egyszerű szövegmező használata helyette.",
"return_to_settings": "Vissza a beállításokhoz"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Atas",
"reply_count": "Jumlah balasan",
"pagination.pageCurrent": "Halaman {{page}} (saat ini)",
"pagination.pageLabel": "Halaman"
"pagination.pageLabel": "Halaman",
"archive": "Arsip",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Peringatan Kunci Privat",
"private_key_warning_description": "Editor ini menampilkan kunci pribadi akun Anda. Jangan bagikan data ini kepada siapa pun. Mengedit dengan salah dapat merusak akun Anda.",
"go_back": "Kembali",
"continue": "Lanjutkan",
"loading_editor": "Memuat editor...",
"editor_fallback_warning": "Editor kode gagal dimuat. Menggunakan textarea biasa sebagai gantinya.",
"return_to_settings": "Kembali ke Pengaturan"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "In alto",
"reply_count": "Numero di risposte",
"pagination.pageCurrent": "Pagina {{page}} (corrente)",
"pagination.pageLabel": "Pagina"
"pagination.pageLabel": "Pagina",
"archive": "Archivio",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Avviso chiave privata",
"private_key_warning_description": "Questo editor mostra la chiave privata del tuo account. Non condividere questi dati con nessuno. Modifiche errate potrebbero compromettere il tuo account.",
"go_back": "Indietro",
"continue": "Continua",
"loading_editor": "Caricamento editor...",
"editor_fallback_warning": "Il caricamento dell'editor di codice non è riuscito. Si usa un campo di testo semplice al suo posto.",
"return_to_settings": "Torna alle impostazioni"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "上",
"reply_count": "返信数",
"pagination.pageCurrent": "{{page}}ページ(現在)",
"pagination.pageLabel": "ページ"
"pagination.pageLabel": "ページ",
"archive": "アーカイブ",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "秘密鍵の警告",
"private_key_warning_description": "このエディターはアカウントの秘密鍵を表示します。このデータを誰とも共有しないでください。誤った編集によりアカウントが破損する可能性があります。",
"go_back": "戻る",
"continue": "続ける",
"loading_editor": "エディターを読み込み中...",
"editor_fallback_warning": "コードエディターの読み込みに失敗しました。代わりにプレーンテキストエリアを使用します。",
"return_to_settings": "設定に戻る"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "맨 위",
"reply_count": "답글 수",
"pagination.pageCurrent": "{{page}}페이지 (현재)",
"pagination.pageLabel": "페이지"
"pagination.pageLabel": "페이지",
"archive": "아카이브",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "개인 키 경고",
"private_key_warning_description": "이 편집기는 계정의 개인 키를 표시합니다. 이 데이터를 누구와도 공유하지 마세요. 잘못된 편집은 계정을 손상시킬 수 있습니다.",
"go_back": "돌아가기",
"continue": "계속",
"loading_editor": "편집기 로딩 중...",
"editor_fallback_warning": "코드 편집기 로드에 실패했습니다. 대신 일반 텍스트 영역을 사용합니다.",
"return_to_settings": "설정으로 돌아가기"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "वर",
"reply_count": "प्रत्युत्तरांची संख्या",
"pagination.pageCurrent": "पृष्ठ {{page}} (वर्तमान)",
"pagination.pageLabel": "पृष्ठ"
"pagination.pageLabel": "पृष्ठ",
"archive": "संग्रह",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "खाजगी कुंजी चेतावनी",
"private_key_warning_description": "हा संपादक तुमच्या खात्याची खाजगी की दर्शवतो. हा डेटा कोणाशीही सामायिक करू नका. चुकीच्या संपादनामुळे तुमचे खाते बिघडू शकते.",
"go_back": "मागे जा",
"continue": "सुरू ठेवा",
"loading_editor": "संपादक लोड होत आहे...",
"editor_fallback_warning": "कोड एडिटर लोड नाही झाला. त्याऐवजी साध्या टेक्स्ट एरियाचा वापर केला जात आहे.",
"return_to_settings": "सेटिंग्जवर परत जा"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Boven",
"reply_count": "Aantal reacties",
"pagination.pageCurrent": "Pagina {{page}} (huidige)",
"pagination.pageLabel": "Pagina"
"pagination.pageLabel": "Pagina",
"archive": "Archief",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Waarschuwing privésleutel",
"private_key_warning_description": "Deze editor toont de privésleutel van je account. Deel deze gegevens met niemand. Onjuiste bewerking kan je account beschadigen.",
"go_back": "Terug",
"continue": "Doorgaan",
"loading_editor": "Editor laden...",
"editor_fallback_warning": "De code-editor kon niet worden geladen. Er wordt een eenvoudig tekstveld gebruikt.",
"return_to_settings": "Terug naar Instellingen"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Topp",
"reply_count": "Antall svar",
"pagination.pageCurrent": "Side {{page}} (nåværende)",
"pagination.pageLabel": "Side"
"pagination.pageLabel": "Side",
"archive": "Arkiv",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Advarsel om privat nøkkel",
"private_key_warning_description": "Denne editoren viser kontoens private nøkkel. Ikke del disse dataene med noen. Feil redigering kan ødelegge kontoen din.",
"go_back": "Gå tilbake",
"continue": "Fortsett",
"loading_editor": "Laster editor...",
"editor_fallback_warning": "Kodeeditoren kunne ikke lastes. Bruker et enkelt tekstfelt i stedet.",
"return_to_settings": "Tilbake til Innstillinger"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Góra",
"reply_count": "Liczba odpowiedzi",
"pagination.pageCurrent": "Strona {{page}} (bieżąca)",
"pagination.pageLabel": "Strona"
"pagination.pageLabel": "Strona",
"archive": "Archiwum",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Ostrzeżenie o kluczu prywatnym",
"private_key_warning_description": "Ten edytor wyświetla klucz prywatny Twojego konta. Nie udostępniaj tych danych nikomu. Nieprawidłowa edycja może uszkodzić Twoje konto.",
"go_back": "Wróć",
"continue": "Kontynuuj",
"loading_editor": "Ładowanie edytora...",
"editor_fallback_warning": "Nie udało się załadować edytora kodu. Używane jest zamiast tego prostego pola tekstowego.",
"return_to_settings": "Powrót do ustawień"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Topo",
"reply_count": "Número de respostas",
"pagination.pageCurrent": "Página {{page}} (atual)",
"pagination.pageLabel": "Página"
"pagination.pageLabel": "Página",
"archive": "Arquivo",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Aviso de chave privada",
"private_key_warning_description": "Este editor mostra a chave privada da sua conta. Não compartilhe esses dados com ninguém. Editar incorretamente pode quebrar sua conta.",
"go_back": "Voltar",
"continue": "Continuar",
"loading_editor": "Carregando editor...",
"editor_fallback_warning": "O editor de código falhou ao carregar. Usando um campo de texto simples em vez disso.",
"return_to_settings": "Voltar às Configurações"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Sus",
"reply_count": "Număr de răspunsuri",
"pagination.pageCurrent": "Pagina {{page}} (curentă)",
"pagination.pageLabel": "Pagină"
"pagination.pageLabel": "Pagină",
"archive": "Arhivă",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Avertisment cheie privată",
"private_key_warning_description": "Acest editor afișează cheia privată a contului tău. Nu partaja aceste date cu nimeni. Editarea incorectă poate deteriora contul tău.",
"go_back": "Înapoi",
"continue": "Continuă",
"loading_editor": "Se încarcă editorul...",
"editor_fallback_warning": "Editorul de cod nu s-a putut încărca. Se folosește un câmp de text simplu în loc.",
"return_to_settings": "Înapoi la Setări"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Вверх",
"reply_count": "Количество ответов",
"pagination.pageCurrent": "Страница {{page}} (текущая)",
"pagination.pageLabel": "Страница"
"pagination.pageLabel": "Страница",
"archive": "Архив",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Предупреждение о закрытом ключе",
"private_key_warning_description": "Этот редактор показывает ваш приватный ключ. Не передавайте эти данные никому. Неправильное редактирование может повредить ваш аккаунт.",
"go_back": "Назад",
"continue": "Продолжить",
"loading_editor": "Загрузка редактора...",
"editor_fallback_warning": "Не удалось загрузить редактор кода. Используется вместо него обычное текстовое поле.",
"return_to_settings": "Вернуться к настройкам"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Lart",
"reply_count": "Numri i përgjigjeve",
"pagination.pageCurrent": "Faqja {{page}} (aktuale)",
"pagination.pageLabel": "Faqe"
"pagination.pageLabel": "Faqe",
"archive": "Arkiv",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Paralajmërim për çelësin privat",
"private_key_warning_description": "Ky redaktor tregon çelësin privat të llogarisë tuaj. Mos ndani këto të dhëna me askënd. Redaktimi gabim mund të dëmtojë llogarinë tuaj.",
"go_back": "Kthehu",
"continue": "Vazhdo",
"loading_editor": "Duke ngarkuar redaktuesin...",
"editor_fallback_warning": "Redaktori i kodit dështoi të ngarkohet. Duke përdorur një fushë teksti të thjeshtë në vend.",
"return_to_settings": "Kthehu te Cilësimet"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Topp",
"reply_count": "Antal svar",
"pagination.pageCurrent": "Sida {{page}} (nuvarande)",
"pagination.pageLabel": "Sida"
"pagination.pageLabel": "Sida",
"archive": "Arkiv",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Varning för privat nyckel",
"private_key_warning_description": "Denna redigerare visar ditt kontos privata nyckel. Del inte dessa data med någon. Felaktig redigering kan skada ditt konto.",
"go_back": "Tillbaka",
"continue": "Fortsätt",
"loading_editor": "Laddar editor...",
"editor_fallback_warning": "Kodredigeraren kunde inte laddas. Använder ett enkelt textfält istället.",
"return_to_settings": "Tillbaka till inställningar"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "పై",
"reply_count": "సమాధానాల సంఖ్య",
"pagination.pageCurrent": "పేజీ {{page}} (ప్రస్తుత)",
"pagination.pageLabel": "పేజీ"
"pagination.pageLabel": "పేజీ",
"archive": "ఆర్కైవ్",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "ప్రైవేట్ కీ హెచ్చరిక",
"private_key_warning_description": "ఈ ఎడిటర్ మీ ఖాతా యొక్క ప్రైవేట్ కీని చూపిస్తుంది. ఈ డేటాను ఎవరితోనూ భాగస్వామ్యం చేయవద్దు. తప్పుగా ఎడిట్ చేయడం మీ ఖాతాను పాడు చేయవచ్చు.",
"go_back": "వెనుకకు",
"continue": "కొనసాగించు",
"loading_editor": "సంపాదకాన్ని లోడ్ చేస్తోంది...",
"editor_fallback_warning": "కోడ్ ఎడిటర్ లోడ్ చేయడం విఫలమైంది. బదులుగా సాధారణ టెక్స్ట్ ఏరియా ఉపయోగిస్తున్నారు.",
"return_to_settings": "సెట్టింగ్‌లకు తిరిగి వెళ్ళండి"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "บน",
"reply_count": "จำนวนการตอบกลับ",
"pagination.pageCurrent": "หน้า {{page}} (ปัจจุบัน)",
"pagination.pageLabel": "หน้า"
"pagination.pageLabel": "หน้า",
"archive": "เก็บถาวร",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "คำเตือนคีย์ส่วนตัว",
"private_key_warning_description": "โปรแกรมแก้ไขนี้แสดงคีย์ส่วนตัวของบัญชีคุณ ห้ามแชร์ข้อมูลนี้กับใครก็ตาม การแก้ไขผิดพลาดอาจทำให้บัญชีของคุณเสียหายได้",
"go_back": "กลับ",
"continue": "ดำเนินการต่อ",
"loading_editor": "กำลังโหลดตัวแก้ไข...",
"editor_fallback_warning": "โหลดตัวแก้ไขโค้ดไม่สำเร็จ กำลังใช้ช่องข้อความธรรมดาแทน",
"return_to_settings": "กลับไปที่การตั้งค่า"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Üst",
"reply_count": "Yanıt sayısı",
"pagination.pageCurrent": "Sayfa {{page}} (mevcut)",
"pagination.pageLabel": "Sayfa"
"pagination.pageLabel": "Sayfa",
"archive": "Arşiv",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Özel anahtar uyarısı",
"private_key_warning_description": "Bu düzenleyici hesabınızın özel anahtarını gösterir. Bu verileri kimseyle paylaşmayın. Yanlış düzenleme hesabınıza zarar verebilir.",
"go_back": "Geri",
"continue": "Devam",
"loading_editor": "Düzenleyici yükleniyor...",
"editor_fallback_warning": "Kod düzenleyici yüklenemedi. Bunun yerine düz bir metin alanı kullanılıyor.",
"return_to_settings": "Ayarlara Dön"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Вгору",
"reply_count": "Кількість відповідей",
"pagination.pageCurrent": "Сторінка {{page}} (поточна)",
"pagination.pageLabel": "Сторінка"
"pagination.pageLabel": "Сторінка",
"archive": "Архів",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Попередження про приватний ключ",
"private_key_warning_description": "Цей редактор показує приватний ключ вашого облікового запису. Не передавайте ці дані нікому. Неправильне редагування може пошкодити ваш обліковий запис.",
"go_back": "Назад",
"continue": "Продовжити",
"loading_editor": "Завантаження редактора...",
"editor_fallback_warning": "Не вдалося завантажити редактор коду. Використовується прості текстове поле замість нього.",
"return_to_settings": "Повернутися до налаштувань"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "اوپر",
"reply_count": "جوابوں کی تعداد",
"pagination.pageCurrent": "صفحہ {{page}} (موجودہ)",
"pagination.pageLabel": "صفحہ"
"pagination.pageLabel": "صفحہ",
"archive": "آرکائیو",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "نجی کلید کی تنبیہ",
"private_key_warning_description": "یہ ایڈیٹر آپ کے اکاؤنٹ کی نجی کلید دکھاتا ہے۔ یہ ڈیٹا کسی کے ساتھ شیئر نہ کریں۔ غلط ترمیم سے آپ کا اکاؤنٹ خراب ہو سکتا ہے۔",
"go_back": "واپس جائیں",
"continue": "جاری رکھیں",
"loading_editor": "ایڈیٹر لوڈ ہو رہا ہے...",
"editor_fallback_warning": "کوڈ ایڈیٹر لوڈ نہیں ہو سکا۔ اس کے بجائے ایک سادہ ٹیکسٹ ایریا استعمال کیا جا رہا ہے۔",
"return_to_settings": "ترتیبات میں واپس جائیں"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "Trên",
"reply_count": "Số lượng phản hồi",
"pagination.pageCurrent": "Trang {{page}} (hiện tại)",
"pagination.pageLabel": "Trang"
"pagination.pageLabel": "Trang",
"archive": "Lưu trữ",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "Cảnh báo khóa riêng tư",
"private_key_warning_description": "Trình chỉnh sửa này hiển thị khóa riêng tư của tài khoản bạn. Không chia sẻ dữ liệu này với bất kỳ ai. Chỉnh sửa sai có thể làm hỏng tài khoản của bạn.",
"go_back": "Quay lại",
"continue": "Tiếp tục",
"loading_editor": "Đang tải trình soạn thảo...",
"editor_fallback_warning": "Không thể tải trình soạn thảo mã. Đang sử dụng ô văn bản thuần thay thế.",
"return_to_settings": "Quay lại Cài đặt"
}
+10 -1
View File
@@ -282,5 +282,14 @@
"top": "顶部",
"reply_count": "回复数",
"pagination.pageCurrent": "第{{page}}页(当前)",
"pagination.pageLabel": "页"
"pagination.pageLabel": "页",
"archive": "归档",
"search_ops_placeholder": "Search OPs...",
"private_key_warning_title": "私钥警告",
"private_key_warning_description": "此编辑器显示您账户的私钥。请勿与任何人分享此数据。错误编辑可能会损坏您的账户。",
"go_back": "返回",
"continue": "继续",
"loading_editor": "正在加载编辑器...",
"editor_fallback_warning": "代码编辑器加载失败。改用纯文本输入框。",
"return_to_settings": "返回设置"
}
+16 -10
View File
@@ -34,6 +34,7 @@ import PostForm from './components/post-form';
import BoardBlotter from './components/board-blotter';
import BoardsBar from './components/boardsbar';
const AccountDataEditor = lazy(() => import('./views/account-data-editor'));
const BoardsBarEditModal = lazy(() => import('./components/boardsbar-edit-modal'));
const CreateBoardModal = lazy(() => import('./components/create-board-modal'));
const DirectoryModal = lazy(() => import('./components/directory-modal'));
@@ -47,7 +48,7 @@ preloadThemeAssets();
const hasModQueueAccessRole = (role?: string): boolean => role === 'admin' || role === 'owner' || role === 'moderator';
const BoardLayout = () => {
const { accountCommentIndex, boardIdentifier } = useParams();
const { accountCommentIndex, boardIdentifier, pageNumber } = useParams();
const location = useLocation();
const isMobile = useIsMobile();
const isInAllView = isAllView(location.pathname);
@@ -83,6 +84,10 @@ const BoardLayout = () => {
? `${subplebbitAddress}-${location.pathname.replace(/\/settings$/, '')}`
: `${subplebbitAddress}-${location.pathname}`;
if (pageNumber === '1') {
return <Navigate to='/not-found' replace />;
}
return (
<div className={styles.boardLayout}>
<BoardsBar />
@@ -180,14 +185,6 @@ const CatalogFeedRoute = () => {
return <Catalog viewType={viewType} boardIdentifier={params.boardIdentifier} />;
};
const PageOneGuard = () => {
const { pageNumber } = useParams();
if (pageNumber === '1') {
return <Navigate to='/not-found' replace />;
}
return null;
};
const ModQueueRoute = () => {
const { boardIdentifier } = useParams();
const account = useAccount();
@@ -244,6 +241,14 @@ const App = () => {
<Route path='/faq' element={<FAQ />} />
<Route path='/rules/:boardIdentifier?' element={<Rules />} />
<Route path='/blotter' element={<Blotter />} />
<Route
path='/settings/account-data'
element={
<Suspense fallback={null}>
<AccountDataEditor />
</Suspense>
}
/>
<Route element={<BoardLayout />}>
{/* Canonical multiboard routes (no time filter) */}
<Route path='/all' element={boardFeedElement} />
@@ -269,7 +274,8 @@ const App = () => {
<Route path='/subs/*' element={<Navigate to='/not-found' replace />} />
<Route path='/mod/*' element={<Navigate to='/not-found' replace />} />
<Route path='/:boardIdentifier/:pageNumber' element={<PageOneGuard />} />
<Route path='/:boardIdentifier/:pageNumber' element={boardFeedElement} />
<Route path='/:boardIdentifier/:pageNumber/settings' element={boardFeedElement} />
<Route path='/:boardIdentifier' element={boardFeedElement} />
<Route path='/:boardIdentifier/settings' element={boardFeedElement} />
<Route path='/:boardIdentifier/catalog' element={catalogFeedElement} />
@@ -0,0 +1,87 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, Root } from 'react-dom/client';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
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>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: { changeLanguage: vi.fn(), language: 'en' },
}),
}));
vi.mock('@plebbit/plebbit-react-hooks', () => ({
useAccount: () => ({ id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } }),
useAccounts: () => ({ accounts: [{ id: 'test-id', name: 'Account 1', author: { shortAddress: '0x1...3' } }] }),
createAccount: vi.fn(),
deleteAccount: vi.fn(),
exportAccount: vi.fn(),
importAccount: vi.fn(),
setActiveAccount: vi.fn(),
}));
vi.mock('@capacitor/core', () => ({
Capacitor: { getPlatform: () => 'web' },
}));
let root: Root;
let container: HTMLDivElement;
const render = (children: React.ReactNode) => {
act(() => {
root.render(createElement(MemoryRouter, { initialEntries: ['/subs/settings'] }, children));
});
};
describe('AccountSettings', () => {
beforeEach(() => {
vi.clearAllMocks();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('does not render an inline textarea', () => {
render(createElement(AccountSettings));
expect(container.querySelector('textarea')).toBeNull();
});
it('does not render save_changes or reset_changes buttons', () => {
render(createElement(AccountSettings));
const buttons = Array.from(container.querySelectorAll('button'));
const buttonTexts = buttons.map((b) => b.textContent ?? '');
expect(buttonTexts.some((t) => t.includes('save_changes'))).toBe(false);
expect(buttonTexts.some((t) => t.includes('reset_changes'))).toBe(false);
});
it('renders an edit button', () => {
render(createElement(AccountSettings));
const buttons = Array.from(container.querySelectorAll('button'));
expect(buttons.some((b) => (b.textContent ?? '').includes('edit'))).toBe(true);
});
it('renders create, import, export buttons', () => {
render(createElement(AccountSettings));
const buttons = Array.from(container.querySelectorAll('button'));
const texts = buttons.map((b) => b.textContent ?? '');
expect(texts.some((t) => t.includes('create'))).toBe(true);
expect(texts.some((t) => t.includes('import'))).toBe(true);
expect(texts.some((t) => t.includes('export'))).toBe(true);
});
it('renders delete_account button', () => {
render(createElement(AccountSettings));
const buttons = Array.from(container.querySelectorAll('button'));
expect(buttons.some((b) => (b.textContent ?? '').includes('delete_account'))).toBe(true);
});
});
@@ -3,34 +3,10 @@
margin-bottom: 10px;
}
.setting textarea {
display: block;
font-family: dejavu sans mono, consolas, andale mono, lucida console, monospace;
outline: none;
border: 1px solid #aaa;
font-size: 11px;
margin: 0 2px 0 0;
padding: 2px 4px 3px;
min-height: 150px;
width: 95%;
}
.setting textarea:focus {
border-color: 1px solid #98e;
}
.setting button {
cursor: pointer;
}
.saveResetAccount {
margin-top: 15px;
}
.setting textarea {
margin-bottom: 5px;
}
.setting div {
margin-top: 5px;
}
@@ -1,7 +1,6 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { createAccount, deleteAccount, exportAccount, importAccount, setAccount, setActiveAccount, useAccount, useAccounts } from '@plebbit/plebbit-react-hooks';
import stringify from 'json-stringify-pretty-compact';
import { createAccount, deleteAccount, exportAccount, importAccount, setActiveAccount, useAccount, useAccounts } from '@plebbit/plebbit-react-hooks';
import styles from './account-settings.module.css';
import { Capacitor } from '@capacitor/core';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -33,24 +32,6 @@ const AccountSettingsEditor = ({
}) => {
const { t } = useTranslation();
const location = useLocation();
const accountJson = useMemo(
() =>
stringify({
account: {
...account,
author: { ...account?.author, avatar: undefined },
plebbit: undefined,
karma: undefined,
plebbitReactOptions: undefined,
unreadNotificationCount: undefined,
},
}),
[account],
);
const [text, setText] = useState(() => accountJson);
const { accounts } = useAccounts();
const switchToNewAccountRef = useRef(false);
const navigate = useNavigate();
@@ -93,29 +74,6 @@ const AccountSettingsEditor = ({
}
};
const saveAccount = async () => {
const parsed = safeParseJSON<{ account: Record<string, unknown> }>(text);
if (!parsed?.account) {
alert('Invalid JSON');
return;
}
const newAccount = parsed.account;
const result = await withErrorHandling(
() => setAccount({ ...newAccount, id: account?.id }),
(error) => {
if (error instanceof Error) {
alert(error.message);
console.log(error);
} else {
console.error('An unknown error occurred:', error);
}
},
);
if (result !== undefined) {
alert(`Saved ${newAccount.name}`);
}
};
const handleExportAccount = async () => {
const accountString = await withErrorHandling(
() => exportAccount(),
@@ -247,10 +205,8 @@ const AccountSettingsEditor = ({
})}
</div>
</div>
<div></div>
<textarea value={text} onChange={(e) => setText(e.target.value)} autoCorrect='off' autoComplete='off' spellCheck='false' />
<div>
<button onClick={saveAccount}>{t('save_changes')}</button> <button onClick={() => setText(accountJson)}>{t('reset_changes')}</button>
<button onClick={() => navigate('/settings/account-data', { state: { returnTo: location.pathname + location.hash } })}>{t('edit')}</button>
<button className={styles.deleteAccount} onClick={() => _deleteAccount(account?.name ?? '')}>
{t('delete_account')}
</button>
@@ -0,0 +1,67 @@
import { describe, it, expect } from 'vitest';
import { buildEditableAccountJson, safeParseAccountJson, buildSavePayload } from '../account-editor-utils';
describe('buildEditableAccountJson', () => {
it('strips runtime-only fields from account', () => {
const account = {
id: 'abc',
name: 'Account 1',
author: { address: '0x123', shortAddress: '0x1...3', avatar: { url: 'https://example.com' } },
plebbit: { someOption: true },
karma: 42,
plebbitReactOptions: { foo: 'bar' },
unreadNotificationCount: 5,
};
const result = JSON.parse(buildEditableAccountJson(account));
expect(result.account.id).toBe('abc');
expect(result.account.name).toBe('Account 1');
expect(result.account.author.address).toBe('0x123');
expect(result.account.author.avatar).toBeUndefined();
expect(result.account.plebbit).toBeUndefined();
expect(result.account.karma).toBeUndefined();
expect(result.account.plebbitReactOptions).toBeUndefined();
expect(result.account.unreadNotificationCount).toBeUndefined();
});
it('handles undefined account', () => {
const result = buildEditableAccountJson(undefined);
expect(result).toBeTruthy();
const parsed = JSON.parse(result);
expect(parsed.account).toBeDefined();
});
});
describe('safeParseAccountJson', () => {
it('parses valid account JSON', () => {
const result = safeParseAccountJson('{"account": {"name": "test"}}');
expect(result).toEqual({ account: { name: 'test' } });
});
it('returns null for invalid JSON', () => {
expect(safeParseAccountJson('not json')).toBeNull();
});
it('returns null for JSON without account key', () => {
expect(safeParseAccountJson('{"name": "test"}')).toBeNull();
});
it('returns null for non-object account', () => {
expect(safeParseAccountJson('{"account": "string"}')).toBeNull();
});
});
describe('buildSavePayload', () => {
it('preserves original account id', () => {
const parsed = { account: { name: 'updated', id: 'wrong-id' } };
const result = buildSavePayload(parsed, 'original-id');
expect(result.id).toBe('original-id');
expect(result.name).toBe('updated');
});
it('handles undefined original id', () => {
const parsed = { account: { name: 'test' } };
const result = buildSavePayload(parsed, undefined);
expect(result.id).toBeUndefined();
expect(result.name).toBe('test');
});
});
+47
View File
@@ -0,0 +1,47 @@
import stringify from 'json-stringify-pretty-compact';
type AccountLike = {
id?: string;
name?: string;
author?: { address?: string; shortAddress?: string; avatar?: unknown };
plebbit?: unknown;
karma?: unknown;
plebbitReactOptions?: unknown;
unreadNotificationCount?: unknown;
[key: string]: unknown;
};
/**
* Build the editable JSON string for an account, stripping runtime-only fields.
*/
export const buildEditableAccountJson = (account: AccountLike | undefined): string =>
stringify({
account: {
...account,
author: { ...account?.author, avatar: undefined },
plebbit: undefined,
karma: undefined,
plebbitReactOptions: undefined,
unreadNotificationCount: undefined,
},
});
export const safeParseAccountJson = (text: string): { account: Record<string, unknown> } | null => {
try {
const parsed = JSON.parse(text);
if (parsed && typeof parsed === 'object' && parsed.account && typeof parsed.account === 'object') {
return parsed as { account: Record<string, unknown> };
}
return null;
} catch {
return null;
}
};
/**
* Build a save-ready account payload, preserving the original account id.
*/
export const buildSavePayload = (parsed: { account: Record<string, unknown> }, originalId: string | undefined): Record<string, unknown> => ({
...parsed.account,
id: originalId,
});
@@ -0,0 +1,79 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, Root } from 'react-dom/client';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import AccountDataEditor from '../account-data-editor';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise<void> }).act as (cb: () => void | Promise<void>) => void | Promise<void>;
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: { changeLanguage: vi.fn(), language: 'en' },
}),
}));
vi.mock('../../../lib/utils/account-editor-utils', () => ({
buildEditableAccountJson: () => '{"account": {"name": "test"}}',
safeParseAccountJson: vi.fn((text: string) => {
try {
const p = JSON.parse(text);
return p?.account ? p : null;
} catch {
return null;
}
}),
buildSavePayload: vi.fn((parsed: { account: Record<string, unknown> }, id: string) => ({ ...parsed.account, id })),
}));
vi.mock('@plebbit/plebbit-react-hooks', () => ({
useAccount: () => ({ id: 'test-id', name: 'Account 1', author: { address: '0x123', shortAddress: '0x1...3' } }),
setAccount: vi.fn(),
}));
let root: Root;
let container: HTMLDivElement;
const render = (children: React.ReactNode) => {
act(() => {
root.render(createElement(MemoryRouter, { initialEntries: ['/settings/account-data'] }, children));
});
};
describe('AccountDataEditor', () => {
beforeEach(() => {
vi.clearAllMocks();
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
it('renders warning gate initially', () => {
render(createElement(AccountDataEditor));
expect(container.textContent).toContain('private_key_warning_title');
});
it('shows go_back and continue buttons in warning gate', () => {
render(createElement(AccountDataEditor));
const buttons = Array.from(container.querySelectorAll('button'));
const texts = buttons.map((b) => b.textContent ?? '');
expect(texts.some((t) => t.includes('go_back'))).toBe(true);
expect(texts.some((t) => t.includes('continue'))).toBe(true);
});
it('does not show editor controls in warning phase', () => {
render(createElement(AccountDataEditor));
const buttons = Array.from(container.querySelectorAll('button'));
const texts = buttons.map((b) => b.textContent ?? '');
expect(texts.some((t) => t.includes('save'))).toBe(false);
expect(texts.some((t) => t.includes('reset_changes'))).toBe(false);
expect(texts.some((t) => t.includes('return_to_settings'))).toBe(false);
});
});
@@ -0,0 +1,54 @@
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.warningGate {
text-align: center;
padding: 40px 20px;
}
.warningTitle {
font-weight: 700;
font-size: 1.2em;
margin-bottom: 12px;
}
.warningDescription {
color: #666;
margin-bottom: 24px;
line-height: 1.5;
}
.warningButtons {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.editorContainer {
font-family: monospace;
font-size: 13px;
}
.controls {
display: flex;
gap: 12px;
margin-top: 16px;
flex-wrap: wrap;
}
.fallbackWarning {
color: #b8860b;
background: #fff8e0;
border: 1px solid #e0c000;
padding: 12px;
margin-bottom: 12px;
}
.loadingMessage {
padding: 20px;
text-align: center;
}
@@ -0,0 +1,119 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { setAccount, useAccount } from '@plebbit/plebbit-react-hooks';
import { buildEditableAccountJson, safeParseAccountJson, buildSavePayload } from '../../lib/utils/account-editor-utils';
import styles from './account-data-editor.module.css';
const DEFAULT_RETURN_TO = '/subs/settings#account-settings';
const loadAce = async () => {
const [aceModule] = await Promise.all([import('react-ace'), import('ace-builds/src-noconflict/mode-json'), import('ace-builds/src-noconflict/theme-monokai')]);
return aceModule.default;
};
const AccountDataEditor = () => {
const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
const account = useAccount();
const returnTo = (location.state as { returnTo?: string } | null)?.returnTo ?? DEFAULT_RETURN_TO;
const [phase, setPhase] = useState<'warning' | 'loading' | 'editor' | 'fallback'>('warning');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [AceEditor, setAceEditor] = useState<React.ComponentType<any> | null>(null);
const [text, setText] = useState('');
useEffect(() => {
if (phase !== 'loading') return;
loadAce()
.then((Editor) => {
setAceEditor(() => Editor);
setText(buildEditableAccountJson(account));
setPhase('editor');
})
.catch(() => {
setText(buildEditableAccountJson(account));
setPhase('fallback');
});
}, [phase, account]);
const handleGoBack = () => navigate(returnTo);
const handleContinue = () => setPhase('loading');
const handleReset = () => setText(buildEditableAccountJson(account));
const handleReturn = () => navigate(returnTo);
const handleSave = async () => {
const parsed = safeParseAccountJson(text);
if (!parsed) {
alert('Invalid JSON');
return;
}
const payload = buildSavePayload(parsed, account?.id);
try {
await setAccount(payload);
navigate(returnTo);
window.location.reload();
} catch (e) {
alert(e instanceof Error ? e.message : 'Error saving');
}
};
if (phase === 'warning') {
return (
<div className={styles.container}>
<div className={styles.warningGate}>
<div className={styles.warningTitle}>{t('private_key_warning_title')}</div>
<div className={styles.warningDescription}>{t('private_key_warning_description')}</div>
<div className={styles.warningButtons}>
<button type='button' onClick={handleGoBack}>
{t('go_back')}
</button>
<button type='button' onClick={handleContinue}>
{t('continue')}
</button>
</div>
</div>
</div>
);
}
if (phase === 'loading') {
return (
<div className={styles.container}>
<div className={styles.loadingMessage}>{t('loading_editor')}</div>
</div>
);
}
return (
<div className={styles.container}>
{phase === 'fallback' && <div className={styles.fallbackWarning}>{t('editor_fallback_warning')}</div>}
<div className={styles.editorContainer}>
{phase === 'editor' && AceEditor ? (
<AceEditor mode='json' theme='monokai' width='100%' height='500px' fontSize={13} showPrintMargin={false} value={text} onChange={setText} />
) : (
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
style={{ width: '100%', height: '500px', fontFamily: 'monospace', fontSize: 13 }}
spellCheck={false}
/>
)}
</div>
<div className={styles.controls}>
<button type='button' onClick={handleSave}>
{t('save')}
</button>
<button type='button' onClick={handleReset}>
{t('reset_changes')}
</button>
<button type='button' onClick={handleReturn}>
{t('return_to_settings')}
</button>
</div>
</div>
);
};
export default AccountDataEditor;
+1
View File
@@ -0,0 +1 @@
export { default } from './account-data-editor';
+29 -103
View File
@@ -3886,18 +3886,6 @@
semver "^7.3.5"
tar "^6.1.11"
"@mintpass/challenge@1.1.10":
version "1.1.10"
resolved "https://registry.yarnpkg.com/@mintpass/challenge/-/challenge-1.1.10.tgz#b32547c55462df4582861e1ae005888056178d74"
integrity sha512-ICi668eSf6nqdwERqXXHWevrO/5CIx4mMFxsS7Ob/IS1lmRr5h7jeAfp4HVofdCkyMw6K61nzjfnhd9rd9AMjQ==
dependencies:
"@keyv/sqlite" "^4.0.5"
env-paths "^3.0.0"
keyv "5.5.0"
multiformats "^13.1.0"
uint8arrays "^4.0.3"
viem "2.45.0"
"@mintpass/challenge@1.1.11":
version "1.1.11"
resolved "https://registry.yarnpkg.com/@mintpass/challenge/-/challenge-1.1.11.tgz#aa2d0d010d53f3d9bf36bf5427c8cb28b05a04bc"
@@ -4528,71 +4516,6 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@plebbit/plebbit-js@https://github.com/plebbit/plebbit-js.git#339f22b29aa670c44b938aa3533529db14261851":
version "0.0.8"
resolved "https://github.com/plebbit/plebbit-js.git#339f22b29aa670c44b938aa3533529db14261851"
dependencies:
"@bonfida/spl-name-service" "3.0.16"
"@enhances/with-resolvers" "0.0.5"
"@helia/block-brokers" "5.0.13"
"@helia/delegated-routing-v1-http-api-client" "5.1.4"
"@helia/ipns" "9.1.5"
"@helia/unixfs" "6.0.4"
"@libp2p/crypto" "5.1.13"
"@libp2p/fetch" "4.0.9"
"@libp2p/gossipsub" "15.0.11"
"@libp2p/identify" "4.0.9"
"@libp2p/interfaces" "3.3.2"
"@libp2p/peer-id" "6.0.4"
"@libp2p/peer-id-factory" "4.2.4"
"@libp2p/websockets" "10.1.2"
"@mintpass/challenge" "1.1.10"
"@plebbit/plebbit-logger" "github:plebbit/plebbit-logger#355a96d7659ed820047980049dfa627d30d83a69"
"@plebbit/proper-lockfile" "github:plebbit/node-proper-lockfile#7fd6332117340c1d3d98dd0afee2d31cc06f72b8"
"@types/proper-lockfile" "4.1.2"
assert "2.1.0"
better-sqlite3 "12.4.1"
blockstore-core "6.1.1"
blockstore-fs "3.0.2"
blockstore-idb "3.0.1"
buffer "6.0.3"
captcha-canvas "3.3.4"
cbor "9.0.1"
debounce "1.2.1"
err-code "3.0.1"
ethers "6.16.0"
ext-name "5.0.0"
helia "6.0.14"
hpagent "1.2.0"
it-all "3.0.6"
jose "4.11.0"
js-sha256 "0.11.1"
js-sha512 "0.9.0"
kubo-rpc-client "6.0.2"
limiter-es6-compat "2.1.2"
localforage "1.10.0"
lodash.merge "4.6.2"
lru-cache "10.1.0"
open-graph-scraper "6.10.0"
p-limit "4.0.0"
p-timeout "6.1.2"
peer-id "0.16.0"
probe-image-size "7.2.3"
remeda "1.57.0"
retry "0.13.1"
rpc-websockets "9.3.1"
safe-stable-stringify "2.4.3"
sha1-uint8array "0.10.7"
skia-canvas "3.0.8"
tcp-port-used "1.0.2"
tiny-typed-emitter "2.1.0"
tinycache "1.1.2"
ts-custom-error "3.3.1"
typestub-ipfs-only-hash "4.0.0"
uuid "11.1.0"
viem "2.45.0"
zod "4.3.6"
"@plebbit/plebbit-js@https://github.com/plebbit/plebbit-js.git#41a631c9ef28631bbd41cc8f3915dd1fa2a9345c":
version "0.0.8"
resolved "https://github.com/plebbit/plebbit-js.git#41a631c9ef28631bbd41cc8f3915dd1fa2a9345c"
@@ -4658,13 +4581,6 @@
viem "2.45.0"
zod "4.3.6"
"@plebbit/plebbit-logger@git+https://github.com/plebbit/plebbit-logger.git":
version "0.0.1"
uid "355a96d7659ed820047980049dfa627d30d83a69"
resolved "git+https://github.com/plebbit/plebbit-logger.git#355a96d7659ed820047980049dfa627d30d83a69"
dependencies:
debug "4.3.4"
"@plebbit/plebbit-logger@github:plebbit/plebbit-logger#355a96d7659ed820047980049dfa627d30d83a69":
version "0.0.1"
uid "355a96d7659ed820047980049dfa627d30d83a69"
@@ -4694,22 +4610,6 @@
uuid "11.1.0"
zustand "4.0.0"
"@plebbit/plebbit-react-hooks@https://github.com/plebbit/plebbit-react-hooks.git#e197af2381ad651e5b74a93bcb705de78f94742c":
version "0.0.1"
resolved "https://github.com/plebbit/plebbit-react-hooks.git#e197af2381ad651e5b74a93bcb705de78f94742c"
dependencies:
"@plebbit/plebbit-js" "https://github.com/plebbit/plebbit-js.git#339f22b29aa670c44b938aa3533529db14261851"
"@plebbit/plebbit-logger" "https://github.com/plebbit/plebbit-logger.git"
assert "2.0.0"
ethers "5.6.9"
localforage "1.10.0"
lodash.isequal "4.5.0"
memoizee "0.4.15"
quick-lru "5.1.1"
uint8arrays "3.1.1"
uuid "11.1.0"
zustand "4.0.0"
"@plebbit/proper-lockfile@github:plebbit/node-proper-lockfile#7fd6332117340c1d3d98dd0afee2d31cc06f72b8":
version "4.1.2"
resolved "https://codeload.github.com/plebbit/node-proper-lockfile/tar.gz/7fd6332117340c1d3d98dd0afee2d31cc06f72b8"
@@ -5810,6 +5710,16 @@ abort-error@^1.0.0, abort-error@^1.0.1:
resolved "https://registry.yarnpkg.com/abort-error/-/abort-error-1.0.1.tgz#526c17caf2ac9eb1fab1ffdff18c5076157a324e"
integrity sha512-fxqCblJiIPdSXIUrxI0PL+eJG49QdP9SQ70qtB65MVAoMr2rASlOyAbJFOylfB467F/f+5BCLJJq58RYi7mGfg==
ace-builds@1.41.0:
version "1.41.0"
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.41.0.tgz#682db7c31624bd42d69be4e975075fb60751ca75"
integrity sha512-tiEUfw7V/FpHuI4tG7KS+muOTMIuPh6zReBAD2Uqhe9t00tLeyVGxjXu0tSqz5OIPWy7/wvuJBVXAsNWx0rYvQ==
ace-builds@^1.36.3:
version "1.43.6"
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.43.6.tgz#6feffd0283da5bbf45a2052470fe0fc3c442831a"
integrity sha512-L1ddibQ7F3vyXR2k2fg+I8TQTPWVA6CKeDQr/h2+8CeyTp3W6EQL8xNFZRTztuP8xNOAqL3IYPqdzs31GCjDvg==
acme-client@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/acme-client/-/acme-client-5.4.0.tgz#989e0625c3ff916d16fb7d7377035e80565aa635"
@@ -7773,6 +7683,11 @@ devlop@^1.0.0:
dependencies:
dequal "^2.0.0"
diff-match-patch@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37"
integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==
diff@^5.0.0, diff@^8.0.3:
version "8.0.3"
resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.3.tgz#c7da3d9e0e8c283bb548681f8d7174653720c2d5"
@@ -11236,12 +11151,12 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.get@^4.0.0:
lodash.get@^4.0.0, lodash.get@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.isequal@4.5.0:
lodash.isequal@4.5.0, lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
@@ -13449,7 +13364,7 @@ prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.0.0, prop-types@^15.7.2:
prop-types@^15.0.0, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -13661,6 +13576,17 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-ace@14.0.1:
version "14.0.1"
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-14.0.1.tgz#dba5761cee92a1e522be7e7dab7584df7e1aa1c7"
integrity sha512-z6YAZ20PNf/FqmYEic//G/UK6uw0rn21g58ASgHJHl9rfE4nITQLqthr9rHMVQK4ezwohJbp2dGrZpkq979PYQ==
dependencies:
ace-builds "^1.36.3"
diff-match-patch "^1.0.5"
lodash.get "^4.4.2"
lodash.isequal "^4.5.0"
prop-types "^15.8.1"
react-doctor@0.0.28:
version "0.0.28"
resolved "https://registry.yarnpkg.com/react-doctor/-/react-doctor-0.0.28.tgz#4c7d6f372e10c3e05508143eb68120ad70d2219f"