From 2995b6abaed7a950ab3afa9e23d4a443ed53dc1b Mon Sep 17 00:00:00 2001 From: jack Date: Sat, 1 Aug 2026 14:14:35 +0300 Subject: [PATCH] Make the panic wipe confirmable and its outcome visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The triple-tap panic wipe fired instantly with no confirmation, on the same 18pt logo whose single tap opens App Info — while the far less destructive triple-tap-to-clear-chat 40pt below it asks first, and the Settings-pane panic button has always confirmed. A fumbled tap on the app's only help affordance could permanently destroy identity, keys, and messages. - Triple-tap now raises the same confirmation dialog the Settings pane uses (one extra tap under duress; zero accidental wipes). - A successful wipe leaves a "all data wiped — new identity created" system message: in duress, "did it work?" must not be a guess — the natural response to uncertainty is tapping again. - A wipe that does not commit (panicRecoveryBlocked) was previously invisible: the app looked wiped, sat silently offline, and keys could still be on disk. It now shows a persistent red banner under the header saying data may remain and a relaunch retries. - Settings/privacy copy no longer says "instantly" (all 30 locales). Co-Authored-By: Claude Fable 5 --- bitchat/App/AppChromeModel.swift | 17 + bitchat/Localizable.xcstrings | 492 +++++++++++++++--- bitchat/ViewModels/ChatViewModel.swift | 7 + .../Components/PanicWipeBlockedBanner.swift | 42 ++ bitchat/Views/ContentHeaderView.swift | 23 +- bitchat/Views/ContentView.swift | 22 +- bitchatTests/AppArchitectureTests.swift | 24 + bitchatTests/ChatViewModelTests.swift | 4 +- 8 files changed, 561 insertions(+), 70 deletions(-) create mode 100644 bitchat/Views/Components/PanicWipeBlockedBanner.swift diff --git a/bitchat/App/AppChromeModel.swift b/bitchat/App/AppChromeModel.swift index 70a6c28c..67c34594 100644 --- a/bitchat/App/AppChromeModel.swift +++ b/bitchat/App/AppChromeModel.swift @@ -18,6 +18,12 @@ final class AppChromeModel: ObservableObject { @Published var bluetoothAlertMessage = "" @Published var bluetoothState: CBManagerState = .unknown @Published var showScreenshotPrivacyWarning = false + /// Triple-tapping the logo asks first; the dialog lives on the header. + @Published var showPanicConfirmation = false + /// Mirrors `ChatViewModel.panicRecoveryBlocked` for the chrome: a wipe + /// that did not commit must be visible, not just logged — the person who + /// triggered it needs to know data may remain on the device. + @Published private(set) var panicWipeBlocked = false private let chatViewModel: ChatViewModel private let onPanicWipe: () -> Void @@ -111,6 +117,13 @@ final class AppChromeModel: ObservableObject { prepareForPanic = preparation } + /// Entry point for the header triple-tap: confirm before destroying. + /// The Settings-pane button has always confirmed; the gesture now goes + /// through the same dialog so a mis-tap can't wipe the device. + func requestPanicWipe() { + showPanicConfirmation = true + } + func panicClearAllData() { prepareForPanic?() onPanicWipe() @@ -145,6 +158,10 @@ final class AppChromeModel: ObservableObject { .receive(on: DispatchQueue.main) .assign(to: &$bluetoothState) + chatViewModel.$panicRecoveryBlocked + .receive(on: DispatchQueue.main) + .assign(to: &$panicWipeBlocked) + hasUnreadPrivateMessages = !privateInboxModel.unreadPeerIDs.isEmpty } } diff --git a/bitchat/Localizable.xcstrings b/bitchat/Localizable.xcstrings index c43da7a2..05af5fb6 100644 --- a/bitchat/Localizable.xcstrings +++ b/bitchat/Localizable.xcstrings @@ -2047,6 +2047,192 @@ } } }, + "content.banner.panic_blocked" : { + "comment" : "Banner shown when a panic wipe did not fully commit; relaunching the app retries the wipe", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "المسح غير مكتمل — قد تبقى بعض البيانات. أغلق bitchat وأعد فتحه لإعادة المحاولة." + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "মুছে ফেলা অসম্পূর্ণ — কিছু ডেটা থেকে যেতে পারে। আবার চেষ্টা করতে bitchat বন্ধ করে আবার খুলুন।" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "löschen unvollständig — einige daten könnten übrig sein. bitchat beenden und erneut öffnen, um es noch einmal zu versuchen." + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "wipe incomplete — some data may remain. quit and reopen bitchat to retry." + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "borrado incompleto — pueden quedar algunos datos. cierra y vuelve a abrir bitchat para reintentar." + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "پاک‌سازی ناقص است — ممکن است برخی داده‌ها باقی مانده باشند. برای تلاش دوباره bitchat را ببندید و دوباره باز کنید." + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "hindi kumpleto ang pagbura — maaaring may natirang data. isara at buksan muli ang bitchat para subukan ulit." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "effacement incomplet — des données peuvent subsister. quittez et rouvrez bitchat pour réessayer." + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "המחיקה לא הושלמה — ייתכן שנשארו נתונים. סגרו ופתחו מחדש את bitchat כדי לנסות שוב." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "मिटाना अधूरा रहा — कुछ डेटा बच सकता है। फिर से कोशिश करने के लिए bitchat बंद करके दोबारा खोलें।" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "penghapusan belum selesai — sebagian data mungkin tersisa. tutup dan buka kembali bitchat untuk mencoba lagi." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "cancellazione incompleta — alcuni dati potrebbero rimanere. chiudi e riapri bitchat per riprovare." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "消去が完了していません — データが残っている可能性があります。bitchat を終了して開き直すと再試行します。" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "삭제가 완료되지 않았습니다 — 일부 데이터가 남아 있을 수 있습니다. bitchat을 종료했다가 다시 열면 재시도합니다." + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "pemadaman tidak lengkap — sebahagian data mungkin masih ada. tutup dan buka semula bitchat untuk mencuba lagi." + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "मेटाउने काम अधूरो — केही डेटा बाँकी रहन सक्छ। पुनः प्रयास गर्न bitchat बन्द गरेर फेरि खोल्नुहोस्।" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wissen onvolledig — er kunnen gegevens achterblijven. sluit bitchat en open het opnieuw om het nogmaals te proberen." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wymazywanie niekompletne — część danych może pozostać. zamknij i otwórz bitchat ponownie, aby spróbować jeszcze raz." + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "limpeza incompleta — alguns dados podem permanecer. feche e reabra o bitchat para tentar novamente." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "limpeza incompleta — alguns dados podem permanecer. feche e reabra o bitchat para tentar novamente." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "стирание не завершено — часть данных могла остаться. закройте и снова откройте bitchat, чтобы повторить." + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "raderingen är ofullständig — viss data kan finnas kvar. avsluta och öppna bitchat igen för att försöka på nytt." + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "அழித்தல் முழுமையடையவில்லை — சில தரவு எஞ்சியிருக்கலாம். மீண்டும் முயற்சிக்க bitchat ஐ மூடி மீண்டும் திறக்கவும்." + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "การล้างข้อมูลไม่สมบูรณ์ — อาจมีข้อมูลหลงเหลืออยู่ ปิดแล้วเปิด bitchat ใหม่เพื่อลองอีกครั้ง" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "silme tamamlanmadı — bazı veriler kalmış olabilir. yeniden denemek için bitchat'i kapatıp tekrar açın." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "стирання не завершено — частина даних могла залишитися. закрийте та знову відкрийте bitchat, щоб повторити." + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "مٹانا مکمل نہیں ہوا — کچھ ڈیٹا باقی رہ سکتا ہے۔ دوبارہ کوشش کے لیے bitchat بند کر کے دوبارہ کھولیں۔" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "xoá chưa hoàn tất — một số dữ liệu có thể còn lại. thoát và mở lại bitchat để thử lại." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除未完成 — 可能仍有数据残留。请退出并重新打开 bitchat 重试。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "清除未完成 — 可能仍有資料殘留。請結束並重新開啟 bitchat 重試。" + } + } + } + }, "content.system.media_delete_refused" : { "comment" : "System message shown in the affected chat when an explicit media delete or /clear was refused and bubbles/files were kept", "extractionState" : "manual", @@ -12115,181 +12301,181 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "اضغط الشعار ثلاث مرات لمسح كل البيانات فوراً" + "value" : "انقر ثلاث مرات على الشعار لمسح جميع البيانات (مع تأكيد)" } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "লোগোতে তিনবার ট্যাপ করলে সঙ্গে সঙ্গে সব ডেটা মুছে যাবে" + "value" : "সব ডেটা মুছতে লোগোতে তিনবার ট্যাপ করুন (নিশ্চিতকরণসহ)" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "tippe dreimal auf das logo, um alle daten sofort zu löschen" + "value" : "dreimal aufs logo tippen, um alle daten zu löschen (mit bestätigung)" } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "triple-tap logo to instantly clear all data" + "value" : "triple-tap logo to wipe all data (asks to confirm)" } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "toca el logotipo tres veces para borrar todos los datos al instante" + "value" : "toca tres veces el logo para borrar todos los datos (con confirmación)" } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "برای پاک کردن فوری همهٔ داده‌ها سه بار روی لوگو بزن" + "value" : "برای پاک کردن همه داده‌ها سه بار روی لوگو بزنید (با تأیید)" } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "i-tap ang logo nang tatlong beses para agad burahin ang lahat ng data" + "value" : "i-tap nang tatlong beses ang logo para burahin ang lahat ng data (may kumpirmasyon)" } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "tape trois fois sur le logo pour tout effacer instantanément" + "value" : "touchez trois fois le logo pour effacer toutes les données (avec confirmation)" } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "הקש על הלוגו שלוש פעמים למחיקת כל הנתונים מייד" + "value" : "הקישו שלוש פעמים על הלוגו כדי למחוק את כל הנתונים (עם אישור)" } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "लोगो पर तीन बार टैप करें और तुरंत सारा डेटा साफ़ करें" + "value" : "सारा डेटा मिटाने के लिए लोगो पर तीन बार टैप करें (पुष्टि के साथ)" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "ketuk logo tiga kali untuk langsung menghapus semua data" + "value" : "ketuk logo tiga kali untuk menghapus semua data (dengan konfirmasi)" } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "tocca il logo tre volte per cancellare subito tutti i dati" + "value" : "tocca tre volte il logo per cancellare tutti i dati (con conferma)" } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "ロゴを3回タップすると全データを即削除" + "value" : "ロゴを3回タップですべてのデータを消去(確認あり)" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "로고를 세 번 탭하여 모든 데이터 즉시 삭제할 수 있습니다" + "value" : "로고를 세 번 탭하면 모든 데이터 삭제 (확인 후 진행)" } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "ketuk logo tiga kali untuk langsung menghapus semua data" + "value" : "ketik logo tiga kali untuk memadamkan semua data (dengan pengesahan)" } }, "ne" : { "stringUnit" : { "state" : "translated", - "value" : "लगो तीन पटक ट्याप गर्दा सबै डाटा तुरुन्त मेटिन्छ" + "value" : "सबै डेटा मेटाउन लोगोमा तीन पटक ट्याप गर्नुहोस् (पुष्टिसहित)" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "tik drie keer op het logo om alle data direct te wissen" + "value" : "tik drie keer op het logo om alle gegevens te wissen (met bevestiging)" } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "stuknij logo trzy razy, aby natychmiast usunąć wszystkie dane" + "value" : "stuknij logo trzy razy, aby wymazać wszystkie dane (z potwierdzeniem)" } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "toca três vezes no logótipo para limpar todos os dados de imediato" + "value" : "toque três vezes no logótipo para apagar todos os dados (com confirmação)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "toque o logo três vezes para limpar todos os dados instantaneamente" + "value" : "toque três vezes no logo para apagar todos os dados (com confirmação)" } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "тройной тап по логотипу мгновенно очищает все данные" + "value" : "трижды коснитесь логотипа, чтобы стереть все данные (с подтверждением)" } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "tryck tre gånger på logotypen för att radera all data direkt" + "value" : "trippeltryck på loggan för att radera all data (med bekräftelse)" } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "எல்லா தரவையும் உடனடியாக நீக்க லோகோவை மூன்று முறைத் தட்டுங்கள்" + "value" : "எல்லா தரவையும் அழிக்க லோகோவை மூன்று முறை தட்டவும் (உறுதிப்படுத்தலுடன்)" } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "แตะโลโก้สามครั้งเพื่อลบข้อมูลทั้งหมดทันที" + "value" : "แตะโลโก้สามครั้งเพื่อล้างข้อมูลทั้งหมด (มีการยืนยัน)" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "logoya üç kez dokunun, tüm veriler anında silinsin" + "value" : "tüm verileri silmek için logoya üç kez dokunun (onaylı)" } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "тричі торкни логотип, щоб миттєво стерти всі дані" + "value" : "тричі торкніться логотипа, щоб стерти всі дані (з підтвердженням)" } }, "ur" : { "stringUnit" : { "state" : "translated", - "value" : "تمام ڈیٹا فوری صاف کرنے کیلئے لوگو پر تین بار ٹیپ کریں" + "value" : "تمام ڈیٹا مٹانے کے لیے لوگو پر تین بار ٹیپ کریں (تصدیق کے ساتھ)" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "chạm logo ba lần để xóa toàn bộ dữ liệu ngay lập tức" + "value" : "chạm ba lần vào logo để xoá toàn bộ dữ liệu (có xác nhận)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "三击标志立即清除全部数据" + "value" : "连点三次标志以清除所有数据(需确认)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "三擊標誌立即清除全部數據" + "value" : "連點三次標誌以清除所有資料(需確認)" } } } @@ -14150,181 +14336,181 @@ "ar" : { "stringUnit" : { "state" : "translated", - "value" : "يمسح كل الرسائل والمفاتيح والهوية. النقر ثلاث مرات على شعار bitchat/ يفعل الشيء نفسه فورًا." + "value" : "يمسح جميع الرسائل والمفاتيح والهوية. النقر ثلاث مرات على شعار bitchat/ يفعل الشيء نفسه." } }, "bn" : { "stringUnit" : { "state" : "translated", - "value" : "সব বার্তা, কী ও পরিচয় মুছে দেয়। bitchat/ লোগোতে তিনবার ট্যাপ করলেও সঙ্গে সঙ্গে একই কাজ হয়।" + "value" : "সব বার্তা, কী এবং পরিচয় মুছে ফেলে। bitchat/ লোগোতে তিনবার ট্যাপ করলেও একই কাজ হয়।" } }, "de" : { "stringUnit" : { "state" : "translated", - "value" : "löscht alle nachrichten, schlüssel und identität. dreimaliges tippen auf das bitchat/-logo macht dasselbe, sofort." + "value" : "löscht alle nachrichten, schlüssel und die identität. dreimaliges tippen aufs bitchat/-logo macht dasselbe." } }, "en" : { "stringUnit" : { "state" : "translated", - "value" : "erases all messages, keys, and identity. triple-tapping the bitchat/ logo does the same, instantly." + "value" : "erases all messages, keys, and identity. triple-tapping the bitchat/ logo does the same." } }, "es" : { "stringUnit" : { "state" : "translated", - "value" : "borra todos los mensajes, claves e identidad. tocar tres veces el logotipo de bitchat/ hace lo mismo, al instante." + "value" : "borra todos los mensajes, claves e identidad. tocar tres veces el logo de bitchat/ hace lo mismo." } }, "fa" : { "stringUnit" : { "state" : "translated", - "value" : "همهٔ پیام‌ها، کلیدها و هویت را پاک می‌کند. سه بار زدن روی لوگوی bitchat/ هم همین کار را فوری انجام می‌دهد." + "value" : "همه پیام‌ها، کلیدها و هویت را پاک می‌کند. سه بار زدن روی لوگوی bitchat/ همین کار را می‌کند." } }, "fil" : { "stringUnit" : { "state" : "translated", - "value" : "binubura ang lahat ng mensahe, key, at pagkakakilanlan. ang pag-tap nang tatlong beses sa logo ng bitchat/ ay pareho ang ginagawa, agad-agad." + "value" : "binubura ang lahat ng mensahe, key, at pagkakakilanlan. ganoon din ang mangyayari kapag na-tap nang tatlong beses ang bitchat/ logo." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "efface tous les messages, les clés et l'identité. taper trois fois sur le logo bitchat/ fait la même chose, instantanément." + "value" : "efface tous les messages, les clés et l'identité. toucher trois fois le logo bitchat/ fait la même chose." } }, "he" : { "stringUnit" : { "state" : "translated", - "value" : "מוחק את כל ההודעות, המפתחות והזהות. הקשה משולשת על הלוגו של bitchat/ עושה את אותו הדבר, מייד." + "value" : "מוחק את כל ההודעות, המפתחות והזהות. הקשה משולשת על הלוגו של bitchat/ עושה את אותו הדבר." } }, "hi" : { "stringUnit" : { "state" : "translated", - "value" : "सभी संदेश, कुंजियाँ और पहचान मिटा देता है। bitchat/ लोगो पर तीन बार टैप करने से भी तुरंत यही होता है।" + "value" : "सभी संदेश, कुंजियाँ और पहचान मिटा देता है। bitchat/ लोगो पर तीन बार टैप करने से भी यही होता है।" } }, "id" : { "stringUnit" : { "state" : "translated", - "value" : "menghapus semua pesan, kunci, dan identitas. mengetuk logo bitchat/ tiga kali melakukan hal yang sama, seketika." + "value" : "menghapus semua pesan, kunci, dan identitas. mengetuk logo bitchat/ tiga kali melakukan hal yang sama." } }, "it" : { "stringUnit" : { "state" : "translated", - "value" : "cancella tutti i messaggi, le chiavi e l'identità. toccare tre volte il logo bitchat/ fa lo stesso, all'istante." + "value" : "cancella tutti i messaggi, le chiavi e l'identità. toccare tre volte il logo bitchat/ fa lo stesso." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "すべてのメッセージ、鍵、アイデンティティを消去します。bitchat/ ロゴを3回タップしても同じことが即座に行われます。" + "value" : "すべてのメッセージ・鍵・アイデンティティを消去します。bitchat/ ロゴの3回タップでも同じことができます。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "모든 메시지, 키, 신원을 지웁니다. bitchat/ 로고를 세 번 탭해도 즉시 같은 동작이 실행됩니다." + "value" : "모든 메시지, 키, 신원을 지웁니다. bitchat/ 로고를 세 번 탭해도 동일합니다." } }, "ms" : { "stringUnit" : { "state" : "translated", - "value" : "menghapus semua pesan, kunci, dan identiti. mengetuk logo bitchat/ tiga kali melakukan perkara yang sama, serta-merta." + "value" : "memadamkan semua mesej, kunci dan identiti. mengetik logo bitchat/ tiga kali melakukan perkara yang sama." } }, "ne" : { "stringUnit" : { "state" : "translated", - "value" : "सबै सन्देश, कुञ्जी र पहिचान मेटाउँछ। bitchat/ लोगोमा तीन पटक ट्याप गर्दा पनि तुरुन्तै त्यही हुन्छ।" + "value" : "सबै सन्देश, कुञ्जी र परिचय मेटाउँछ। bitchat/ लोगोमा तीन पटक ट्याप गर्दा पनि त्यही हुन्छ।" } }, "nl" : { "stringUnit" : { "state" : "translated", - "value" : "wist alle berichten, sleutels en identiteit. drie keer tikken op het bitchat/-logo doet hetzelfde, direct." + "value" : "wist alle berichten, sleutels en identiteit. drie keer tikken op het bitchat/-logo doet hetzelfde." } }, "pl" : { "stringUnit" : { "state" : "translated", - "value" : "usuwa wszystkie wiadomości, klucze i tożsamość. trzykrotne stuknięcie logo bitchat/ robi to samo, natychmiast." + "value" : "wymazuje wszystkie wiadomości, klucze i tożsamość. trzykrotne stuknięcie logo bitchat/ robi to samo." } }, "pt" : { "stringUnit" : { "state" : "translated", - "value" : "apaga todas as mensagens, chaves e identidade. tocar três vezes no logótipo bitchat/ faz o mesmo, de imediato." + "value" : "apaga todas as mensagens, chaves e identidade. tocar três vezes no logótipo bitchat/ faz o mesmo." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", - "value" : "apaga todas as mensagens, chaves e identidade. tocar três vezes no logo bitchat/ faz o mesmo, instantaneamente." + "value" : "apaga todas as mensagens, chaves e identidade. tocar três vezes no logo bitchat/ faz o mesmo." } }, "ru" : { "stringUnit" : { "state" : "translated", - "value" : "стирает все сообщения, ключи и личность. тройной тап по логотипу bitchat/ делает то же самое, мгновенно." + "value" : "стирает все сообщения, ключи и идентичность. тройное касание логотипа bitchat/ делает то же самое." } }, "sv" : { "stringUnit" : { "state" : "translated", - "value" : "raderar alla meddelanden, nycklar och identitet. att trycka tre gånger på bitchat/-logotypen gör detsamma, direkt." + "value" : "raderar alla meddelanden, nycklar och identitet. att trippeltrycka på bitchat/-loggan gör samma sak." } }, "ta" : { "stringUnit" : { "state" : "translated", - "value" : "எல்லா செய்திகள், சாவிகள், அடையாளத்தையும் அழிக்கிறது. bitchat/ லோகோவை மூன்று முறைத் தட்டினாலும் உடனடியாக அதுவே நடக்கும்." + "value" : "எல்லா செய்திகள், விசைகள் மற்றும் அடையாளத்தை அழிக்கிறது. bitchat/ லோகோவை மூன்று முறை தட்டினாலும் இதுவே நடக்கும்." } }, "th" : { "stringUnit" : { "state" : "translated", - "value" : "ลบข้อความ กุญแจ และตัวตนทั้งหมด การแตะโลโก้ bitchat/ สามครั้งก็ทำแบบเดียวกันทันที" + "value" : "ลบข้อความ กุญแจ และตัวตนทั้งหมด การแตะโลโก้ bitchat/ สามครั้งให้ผลเหมือนกัน" } }, "tr" : { "stringUnit" : { "state" : "translated", - "value" : "tüm mesajları, anahtarları ve kimliği siler. bitchat/ logosuna üç kez dokunmak da aynısını anında yapar." + "value" : "tüm mesajları, anahtarları ve kimliği siler. bitchat/ logosuna üç kez dokunmak da aynısını yapar." } }, "uk" : { "stringUnit" : { "state" : "translated", - "value" : "стирає всі повідомлення, ключі та особистість. потрійний дотик до логотипа bitchat/ робить те саме, миттєво." + "value" : "стирає всі повідомлення, ключі та ідентичність. потрійне торкання логотипа bitchat/ робить те саме." } }, "ur" : { "stringUnit" : { "state" : "translated", - "value" : "تمام پیغامات، کلیدیں اور شناخت مٹا دیتا ہے۔ bitchat/ لوگو پر تین بار ٹیپ کرنے سے بھی فوراً یہی ہوتا ہے۔" + "value" : "تمام پیغامات، کلیدیں اور شناخت مٹا دیتا ہے۔ bitchat/ لوگو پر تین بار ٹیپ کرنے سے بھی یہی ہوتا ہے۔" } }, "vi" : { "stringUnit" : { "state" : "translated", - "value" : "xóa toàn bộ tin nhắn, khóa và danh tính. chạm ba lần vào logo bitchat/ cũng làm điều tương tự, ngay lập tức." + "value" : "xoá toàn bộ tin nhắn, khoá và danh tính. chạm ba lần vào logo bitchat/ cũng làm điều tương tự." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "抹除所有消息、密钥和身份。三击 bitchat/ 标志也会立即执行相同操作。" + "value" : "清除所有消息、密钥和身份。连点三次 bitchat/ 标志效果相同。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "抹除所有訊息、金鑰和身分。三擊 bitchat/ 標誌也會立即執行相同操作。" + "value" : "清除所有訊息、金鑰和身分。連點三次 bitchat/ 標誌效果相同。" } } } @@ -66798,6 +66984,192 @@ } } }, + "system.panic.completed" : { + "comment" : "System message confirming a successful panic wipe", + "extractionState" : "manual", + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "تم مسح جميع البيانات — تم إنشاء هوية جديدة" + } + }, + "bn" : { + "stringUnit" : { + "state" : "translated", + "value" : "সব ডেটা মুছে ফেলা হয়েছে — নতুন পরিচয় তৈরি হয়েছে" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "alle daten gelöscht — neue identität erstellt" + } + }, + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "all data wiped — new identity created" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "todos los datos borrados — nueva identidad creada" + } + }, + "fa" : { + "stringUnit" : { + "state" : "translated", + "value" : "همه داده‌ها پاک شد — هویت جدید ساخته شد" + } + }, + "fil" : { + "stringUnit" : { + "state" : "translated", + "value" : "nabura na ang lahat ng data — gumawa ng bagong pagkakakilanlan" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "toutes les données effacées — nouvelle identité créée" + } + }, + "he" : { + "stringUnit" : { + "state" : "translated", + "value" : "כל הנתונים נמחקו — נוצרה זהות חדשה" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "सारा डेटा मिटा दिया गया — नई पहचान बनाई गई" + } + }, + "id" : { + "stringUnit" : { + "state" : "translated", + "value" : "semua data dihapus — identitas baru dibuat" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "tutti i dati cancellati — nuova identità creata" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "すべてのデータを消去しました — 新しいアイデンティティを作成しました" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "모든 데이터 삭제됨 — 새 신원 생성됨" + } + }, + "ms" : { + "stringUnit" : { + "state" : "translated", + "value" : "semua data dipadamkan — identiti baharu dicipta" + } + }, + "ne" : { + "stringUnit" : { + "state" : "translated", + "value" : "सबै डेटा मेटाइयो — नयाँ परिचय सिर्जना गरियो" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "alle gegevens gewist — nieuwe identiteit aangemaakt" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "wszystkie dane wymazane — utworzono nową tożsamość" + } + }, + "pt" : { + "stringUnit" : { + "state" : "translated", + "value" : "todos os dados apagados — nova identidade criada" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "todos os dados apagados — nova identidade criada" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "все данные стёрты — создана новая идентичность" + } + }, + "sv" : { + "stringUnit" : { + "state" : "translated", + "value" : "all data raderad — ny identitet skapad" + } + }, + "ta" : { + "stringUnit" : { + "state" : "translated", + "value" : "எல்லா தரவும் அழிக்கப்பட்டது — புதிய அடையாளம் உருவாக்கப்பட்டது" + } + }, + "th" : { + "stringUnit" : { + "state" : "translated", + "value" : "ล้างข้อมูลทั้งหมดแล้ว — สร้างตัวตนใหม่แล้ว" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "tüm veriler silindi — yeni kimlik oluşturuldu" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "усі дані стерто — створено нову ідентичність" + } + }, + "ur" : { + "stringUnit" : { + "state" : "translated", + "value" : "تمام ڈیٹا مٹا دیا گیا — نئی شناخت بنائی گئی" + } + }, + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "đã xoá toàn bộ dữ liệu — đã tạo danh tính mới" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已清除所有数据 — 已创建新身份" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已清除所有資料 — 已建立新身分" + } + } + } + }, "system.chat.blocked" : { "extractionState" : "manual", "localizations" : { diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 12adfda9..7dd3dd8f 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -1771,6 +1771,13 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage panicNetworkLifecycle.restart() } + // In a duress scenario "did it work?" must not be a guess — the + // natural response to uncertainty is to trigger the wipe again. + // The failure case surfaces separately via `panicRecoveryBlocked`. + addMeshOnlySystemMessage( + String(localized: "system.panic.completed", defaultValue: "all data wiped — new identity created", comment: "System message confirming a successful panic wipe") + ) + return true } diff --git a/bitchat/Views/Components/PanicWipeBlockedBanner.swift b/bitchat/Views/Components/PanicWipeBlockedBanner.swift new file mode 100644 index 00000000..9039caf0 --- /dev/null +++ b/bitchat/Views/Components/PanicWipeBlockedBanner.swift @@ -0,0 +1,42 @@ +// +// PanicWipeBlockedBanner.swift +// bitchat +// +// This is free and unencumbered software released into the public domain. +// For more information, see +// + +import SwiftUI + +/// Shown under the header while a panic wipe has not committed +/// (`ChatViewModel.panicRecoveryBlocked`). A wipe that fails must be as loud +/// as the wipe itself: the person who triggered it may be relying on the +/// device being clean, and networking stays disabled until a relaunch retries +/// the transaction — without this banner the app just looks silently dead. +struct PanicWipeBlockedBanner: View { + @ThemedPalette private var palette + + private var message: String { + String( + localized: "content.banner.panic_blocked", + defaultValue: "wipe incomplete — some data may remain. quit and reopen bitchat to retry.", + comment: "Banner shown when a panic wipe did not fully commit; relaunching the app retries the wipe" + ) + } + + var body: some View { + HStack(alignment: .firstTextBaseline, spacing: 6) { + Image(systemName: "exclamationmark.triangle.fill") + .bitchatFont(size: 11, weight: .semibold) + Text(message) + .bitchatFont(size: 11) + .fixedSize(horizontal: false, vertical: true) + } + .foregroundColor(palette.alertRed) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .background(Color.red.opacity(0.12)) + .accessibilityElement(children: .combine) + } +} diff --git a/bitchat/Views/ContentHeaderView.swift b/bitchat/Views/ContentHeaderView.swift index bd3af7f2..a4bf074a 100644 --- a/bitchat/Views/ContentHeaderView.swift +++ b/bitchat/Views/ContentHeaderView.swift @@ -51,11 +51,32 @@ struct ContentHeaderView: View { // cluster at priority 3 never gives up width. .layoutPriority(2) .onTapGesture(count: 3) { - appChromeModel.panicClearAllData() + // Confirm before destroying: the same logo is the single-tap + // App Info entry point and sits beside the nickname field, so + // a fumbled tap must never be able to wipe the device. The + // dialog matches the Settings-pane panic button; under duress + // it costs one extra tap. + appChromeModel.requestPanicWipe() } .onTapGesture(count: 1) { appChromeModel.presentAppInfo() } + .confirmationDialog( + Text( + String(localized: "app_info.settings.danger.panic_confirm_title", defaultValue: "wipe all data?", comment: "Title of the confirmation dialog before a panic wipe") + ), + isPresented: $appChromeModel.showPanicConfirmation, + titleVisibility: .visible + ) { + Button(role: .destructive) { + appChromeModel.panicClearAllData() + } label: { + Text( + String(localized: "app_info.settings.danger.panic_confirm_action", defaultValue: "wipe everything", comment: "Destructive confirmation button that performs the panic wipe") + ) + } + Button("common.cancel", role: .cancel) {} + } // This is the only entry point to App Info, but it reads as // static text; surface the tap. (The triple-tap panic wipe // stays undiscoverable on purpose — it's destructive.) diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 1fe95c75..a04ea03e 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -489,14 +489,20 @@ struct ContentView: View { } private var headerView: some View { - ContentHeaderView( - showSidebar: $showSidebar, - showVerifySheet: $showVerifySheet, - isNicknameFieldFocused: $isNicknameFieldFocused, - headerHeight: headerHeight, - headerPeerIconSize: headerPeerIconSize, - headerPeerCountFontSize: headerPeerCountFontSize - ) + VStack(spacing: 0) { + ContentHeaderView( + showSidebar: $showSidebar, + showVerifySheet: $showVerifySheet, + isNicknameFieldFocused: $isNicknameFieldFocused, + headerHeight: headerHeight, + headerPeerIconSize: headerPeerIconSize, + headerPeerCountFontSize: headerPeerCountFontSize + ) + + if appChromeModel.panicWipeBlocked { + PanicWipeBlockedBanner() + } + } } private var publicMessageList: some View { diff --git a/bitchatTests/AppArchitectureTests.swift b/bitchatTests/AppArchitectureTests.swift index 2fdb94df..941ef461 100644 --- a/bitchatTests/AppArchitectureTests.swift +++ b/bitchatTests/AppArchitectureTests.swift @@ -458,6 +458,30 @@ struct AppArchitectureTests { #expect(chromeModel.showingFingerprintFor == nil) } + @Test("Triple-tap panic entry point only raises the confirmation dialog") + @MainActor + func requestPanicWipeAsksBeforeDestroying() { + let viewModel = makeArchitectureViewModel() + let privateInboxModel = PrivateInboxModel(conversations: ConversationStore()) + let chromeModel = AppChromeModel(chatViewModel: viewModel, privateInboxModel: privateInboxModel) + viewModel.seedPublicMessages([ + BitchatMessage( + id: "keep-1", + sender: "Tester", + content: "still here", + timestamp: Date(), + isRelay: false + ) + ]) + + chromeModel.requestPanicWipe() + + // The gesture must never wipe directly — a fumbled tap on the logo + // (single-tap opens App Info) cannot be allowed to destroy identity. + #expect(chromeModel.showPanicConfirmation) + #expect(viewModel.messages.map(\.id) == ["keep-1"]) + } + @Test("PrivateConversationModel resolves canonical header state for the selected DM") @MainActor func privateConversationModelResolvesSelectedHeaderState() async { diff --git a/bitchatTests/ChatViewModelTests.swift b/bitchatTests/ChatViewModelTests.swift index 7e4e46a1..274b87e0 100644 --- a/bitchatTests/ChatViewModelTests.swift +++ b/bitchatTests/ChatViewModelTests.swift @@ -2261,7 +2261,9 @@ struct ChatViewModelPanicTests { // After panic, emergency disconnect should be called #expect(transport.emergencyDisconnectCallCount == 1) - #expect(viewModel.messages.isEmpty) + // Pre-panic content is gone; the only survivor is the system message + // confirming the wipe (in duress, "did it work?" must not be a guess). + #expect(viewModel.messages.map(\.sender) == ["system"]) #expect(viewModel.privateChats.isEmpty) #expect(viewModel.unreadPrivateMessages.isEmpty) #expect(viewModel.selectedPrivateChatPeer == nil)