Make the panic wipe confirmable and its outcome visible

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 <noreply@anthropic.com>
This commit is contained in:
jack 2026-08-01 14:14:35 +03:00
parent 681c180060
commit 2995b6abae
8 changed files with 561 additions and 70 deletions

View File

@ -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
}
}

View File

@ -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" : {

View File

@ -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
}

View File

@ -0,0 +1,42 @@
//
// PanicWipeBlockedBanner.swift
// bitchat
//
// This is free and unencumbered software released into the public domain.
// For more information, see <https://unlicense.org>
//
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)
}
}

View File

@ -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.)

View File

@ -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 {

View File

@ -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 {

View File

@ -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)