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 bluetoothAlertMessage = ""
@Published var bluetoothState: CBManagerState = .unknown @Published var bluetoothState: CBManagerState = .unknown
@Published var showScreenshotPrivacyWarning = false @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 chatViewModel: ChatViewModel
private let onPanicWipe: () -> Void private let onPanicWipe: () -> Void
@ -111,6 +117,13 @@ final class AppChromeModel: ObservableObject {
prepareForPanic = preparation 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() { func panicClearAllData() {
prepareForPanic?() prepareForPanic?()
onPanicWipe() onPanicWipe()
@ -145,6 +158,10 @@ final class AppChromeModel: ObservableObject {
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.assign(to: &$bluetoothState) .assign(to: &$bluetoothState)
chatViewModel.$panicRecoveryBlocked
.receive(on: DispatchQueue.main)
.assign(to: &$panicWipeBlocked)
hasUnreadPrivateMessages = !privateInboxModel.unreadPeerIDs.isEmpty 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" : { "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", "comment" : "System message shown in the affected chat when an explicit media delete or /clear was refused and bubbles/files were kept",
"extractionState" : "manual", "extractionState" : "manual",
@ -12115,181 +12301,181 @@
"ar" : { "ar" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "اضغط الشعار ثلاث مرات لمسح كل البيانات فوراً" "value" : "انقر ثلاث مرات على الشعار لمسح جميع البيانات (مع تأكيد)"
} }
}, },
"bn" : { "bn" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "লোগোতে তিনবার ট্যাপ করলে সঙ্গে সঙ্গে সব ডেটা মুছে যাবে" "value" : "সব ডেটা মুছতে লোগোতে তিনবার ট্যাপ করুন (নিশ্চিতকরণসহ)"
} }
}, },
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "triple-tap logo to instantly clear all data" "value" : "triple-tap logo to wipe all data (asks to confirm)"
} }
}, },
"es" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "fa" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "برای پاک کردن فوری همهٔ داده‌ها سه بار روی لوگو بزن" "value" : "برای پاک کردن همه داده‌ها سه بار روی لوگو بزنید (با تأیید)"
} }
}, },
"fil" : { "fil" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "fr" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "he" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "הקש על הלוגו שלוש פעמים למחיקת כל הנתונים מייד" "value" : "הקישו שלוש פעמים על הלוגו כדי למחוק את כל הנתונים (עם אישור)"
} }
}, },
"hi" : { "hi" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "लोगो पर तीन बार टैप करें और तुरंत सारा डेटा साफ़ करें" "value" : "सारा डेटा मिटाने के लिए लोगो पर तीन बार टैप करें (पुष्टि के साथ)"
} }
}, },
"id" : { "id" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "ketuk logo tiga kali untuk langsung menghapus semua data" "value" : "ketuk logo tiga kali untuk menghapus semua data (dengan konfirmasi)"
} }
}, },
"it" : { "it" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "ja" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "ロゴを3回タップすると全データを即削除" "value" : "ロゴを3回タップですべてのデータを消去(確認あり)"
} }
}, },
"ko" : { "ko" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "로고를 세 번 탭하여 모든 데이터 즉시 삭제할 수 있습니다" "value" : "로고를 세 번 탭하면 모든 데이터 삭제 (확인 후 진행)"
} }
}, },
"ms" : { "ms" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "ketuk logo tiga kali untuk langsung menghapus semua data" "value" : "ketik logo tiga kali untuk memadamkan semua data (dengan pengesahan)"
} }
}, },
"ne" : { "ne" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "लगो तीन पटक ट्याप गर्दा सबै डाटा तुरुन्त मेटिन्छ" "value" : "सबै डेटा मेटाउन लोगोमा तीन पटक ट्याप गर्नुहोस् (पुष्टिसहित)"
} }
}, },
"nl" : { "nl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "pl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "stuknij logo trzy razy, aby natychmiast usunąć wszystkie dane" "value" : "stuknij logo trzy razy, aby wymazać wszystkie dane (z potwierdzeniem)"
} }
}, },
"pt" : { "pt" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "pt-BR" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "ru" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "тройной тап по логотипу мгновенно очищает все данные" "value" : "трижды коснитесь логотипа, чтобы стереть все данные (с подтверждением)"
} }
}, },
"sv" : { "sv" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "ta" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "எல்லா தரவையும் உடனடியாக நீக்க லோகோவை மூன்று முறைத் தட்டுங்கள்" "value" : "எல்லா தரவையும் அழிக்க லோகோவை மூன்று முறை தட்டவும் (உறுதிப்படுத்தலுடன்)"
} }
}, },
"th" : { "th" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "แตะโลโก้สามครั้งเพื่อลบข้อมูลทั้งหมดทันที" "value" : "แตะโลโก้สามครั้งเพื่อล้างข้อมูลทั้งหมด (มีการยืนยัน)"
} }
}, },
"tr" : { "tr" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "logoya üç kez dokunun, tüm veriler anında silinsin" "value" : "tüm verileri silmek için logoya üç kez dokunun (onaylı)"
} }
}, },
"uk" : { "uk" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "тричі торкни логотип, щоб миттєво стерти всі дані" "value" : "тричі торкніться логотипа, щоб стерти всі дані (з підтвердженням)"
} }
}, },
"ur" : { "ur" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "تمام ڈیٹا فوری صاف کرنے کیلئے لوگو پر تین بار ٹیپ کریں" "value" : "تمام ڈیٹا مٹانے کے لیے لوگو پر تین بار ٹیپ کریں (تصدیق کے ساتھ)"
} }
}, },
"vi" : { "vi" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "三击标志立即清除全部数据" "value" : "连点三次标志以清除所有数据(需确认)"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "三擊標誌立即清除全部數據" "value" : "連點三次標誌以清除所有資料(需確認)"
} }
} }
} }
@ -14150,181 +14336,181 @@
"ar" : { "ar" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "يمسح كل الرسائل والمفاتيح والهوية. النقر ثلاث مرات على شعار bitchat/ يفعل الشيء نفسه فورًا." "value" : "يمسح جميع الرسائل والمفاتيح والهوية. النقر ثلاث مرات على شعار bitchat/ يفعل الشيء نفسه."
} }
}, },
"bn" : { "bn" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "সব বার্তা, কী ও পরিচয় মুছে দেয়। bitchat/ লোগোতে তিনবার ট্যাপ করলেও সঙ্গে সঙ্গে একই কাজ হয়।" "value" : "সব বার্তা, কী এবং পরিচয় মুছে ফেলে। bitchat/ লোগোতে তিনবার ট্যাপ করলেও একই কাজ হয়।"
} }
}, },
"de" : { "de" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "en" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "es" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "fa" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "همهٔ پیام‌ها، کلیدها و هویت را پاک می‌کند. سه بار زدن روی لوگوی bitchat/ هم همین کار را فوری انجام می‌دهد." "value" : "همه پیام‌ها، کلیدها و هویت را پاک می‌کند. سه بار زدن روی لوگوی bitchat/ همین کار را می‌کند."
} }
}, },
"fil" : { "fil" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "fr" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "he" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "מוחק את כל ההודעות, המפתחות והזהות. הקשה משולשת על הלוגו של bitchat/ עושה את אותו הדבר, מייד." "value" : "מוחק את כל ההודעות, המפתחות והזהות. הקשה משולשת על הלוגו של bitchat/ עושה את אותו הדבר."
} }
}, },
"hi" : { "hi" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "सभी संदेश, कुंजियाँ और पहचान मिटा देता है। bitchat/ लोगो पर तीन बार टैप करने से भी तुरंत यही होता है।" "value" : "सभी संदेश, कुंजियाँ और पहचान मिटा देता है। bitchat/ लोगो पर तीन बार टैप करने से भी यही होता है।"
} }
}, },
"id" : { "id" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "it" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "ja" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "すべてのメッセージ、鍵、アイデンティティを消去します。bitchat/ ロゴを3回タップしても同じことが即座に行われます。" "value" : "すべてのメッセージ・鍵・アイデンティティを消去します。bitchat/ ロゴの3回タップでも同じことができます。"
} }
}, },
"ko" : { "ko" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "모든 메시지, 키, 신원을 지웁니다. bitchat/ 로고를 세 번 탭해도 즉시 같은 동작이 실행됩니다." "value" : "모든 메시지, 키, 신원을 지웁니다. bitchat/ 로고를 세 번 탭해도 동일합니다."
} }
}, },
"ms" : { "ms" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "ne" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "सबै सन्देश, कुञ्जी र पहिचान मेटाउँछ। bitchat/ लोगोमा तीन पटक ट्याप गर्दा पनि तुरुन्तै त्यही हुन्छ।" "value" : "सबै सन्देश, कुञ्जी र परिचय मेटाउँछ। bitchat/ लोगोमा तीन पटक ट्याप गर्दा पनि त्यही हुन्छ।"
} }
}, },
"nl" : { "nl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "pl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "pt" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "pt-BR" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "ru" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "стирает все сообщения, ключи и личность. тройной тап по логотипу bitchat/ делает то же самое, мгновенно." "value" : "стирает все сообщения, ключи и идентичность. тройное касание логотипа bitchat/ делает то же самое."
} }
}, },
"sv" : { "sv" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "ta" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "எல்லா செய்திகள், சாவிகள், அடையாளத்தையும் அழிக்கிறது. bitchat/ லோகோவை மூன்று முறைத் தட்டினாலும் உடனடியாக அதுவே நடக்கும்." "value" : "எல்லா செய்திகள், விசைகள் மற்றும் அடையாளத்தை அழிக்கிறது. bitchat/ லோகோவை மூன்று முறை தட்டினாலும் இதுவே நடக்கும்."
} }
}, },
"th" : { "th" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "ลบข้อความ กุญแจ และตัวตนทั้งหมด การแตะโลโก้ bitchat/ สามครั้งก็ทำแบบเดียวกันทันที" "value" : "ลบข้อความ กุญแจ และตัวตนทั้งหมด การแตะโลโก้ bitchat/ สามครั้งให้ผลเหมือนกัน"
} }
}, },
"tr" : { "tr" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "uk" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "стирає всі повідомлення, ключі та особистість. потрійний дотик до логотипа bitchat/ робить те саме, миттєво." "value" : "стирає всі повідомлення, ключі та ідентичність. потрійне торкання логотипа bitchat/ робить те саме."
} }
}, },
"ur" : { "ur" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "تمام پیغامات، کلیدیں اور شناخت مٹا دیتا ہے۔ bitchat/ لوگو پر تین بار ٹیپ کرنے سے بھی فوراً یہی ہوتا ہے۔" "value" : "تمام پیغامات، کلیدیں اور شناخت مٹا دیتا ہے۔ bitchat/ لوگو پر تین بار ٹیپ کرنے سے بھی یہی ہوتا ہے۔"
} }
}, },
"vi" : { "vi" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "zh-Hans" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "抹除所有消息、密钥和身份。三击 bitchat/ 标志也会立即执行相同操作。" "value" : "清除所有消息、密钥和身份。连点三次 bitchat/ 标志效果相同。"
} }
}, },
"zh-Hant" : { "zh-Hant" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "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" : { "system.chat.blocked" : {
"extractionState" : "manual", "extractionState" : "manual",
"localizations" : { "localizations" : {

View File

@ -1771,6 +1771,13 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
panicNetworkLifecycle.restart() 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 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. // cluster at priority 3 never gives up width.
.layoutPriority(2) .layoutPriority(2)
.onTapGesture(count: 3) { .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) { .onTapGesture(count: 1) {
appChromeModel.presentAppInfo() 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 // This is the only entry point to App Info, but it reads as
// static text; surface the tap. (The triple-tap panic wipe // static text; surface the tap. (The triple-tap panic wipe
// stays undiscoverable on purpose it's destructive.) // stays undiscoverable on purpose it's destructive.)

View File

@ -489,14 +489,20 @@ struct ContentView: View {
} }
private var headerView: some View { private var headerView: some View {
ContentHeaderView( VStack(spacing: 0) {
showSidebar: $showSidebar, ContentHeaderView(
showVerifySheet: $showVerifySheet, showSidebar: $showSidebar,
isNicknameFieldFocused: $isNicknameFieldFocused, showVerifySheet: $showVerifySheet,
headerHeight: headerHeight, isNicknameFieldFocused: $isNicknameFieldFocused,
headerPeerIconSize: headerPeerIconSize, headerHeight: headerHeight,
headerPeerCountFontSize: headerPeerCountFontSize headerPeerIconSize: headerPeerIconSize,
) headerPeerCountFontSize: headerPeerCountFontSize
)
if appChromeModel.panicWipeBlocked {
PanicWipeBlockedBanner()
}
}
} }
private var publicMessageList: some View { private var publicMessageList: some View {

View File

@ -458,6 +458,30 @@ struct AppArchitectureTests {
#expect(chromeModel.showingFingerprintFor == nil) #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") @Test("PrivateConversationModel resolves canonical header state for the selected DM")
@MainActor @MainActor
func privateConversationModelResolvesSelectedHeaderState() async { func privateConversationModelResolvesSelectedHeaderState() async {

View File

@ -2261,7 +2261,9 @@ struct ChatViewModelPanicTests {
// After panic, emergency disconnect should be called // After panic, emergency disconnect should be called
#expect(transport.emergencyDisconnectCallCount == 1) #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.privateChats.isEmpty)
#expect(viewModel.unreadPrivateMessages.isEmpty) #expect(viewModel.unreadPrivateMessages.isEmpty)
#expect(viewModel.selectedPrivateChatPeer == nil) #expect(viewModel.selectedPrivateChatPeer == nil)