QR and Verification feature (#529)

* Automated update of relay data - Sun Sep 21 06:21:05 UTC 2025

* Automated update of relay data - Sun Sep 28 06:20:40 UTC 2025

* refactor: new close button like ios(but not liquid glass)

* Automated update of relay data - Sun Oct  5 06:20:09 UTC 2025

* Automated update of relay data - Sun Oct 12 06:20:12 UTC 2025

* Automated update of relay data - Sun Oct 19 06:21:51 UTC 2025

* Automated update of relay data - Sun Oct 26 06:21:31 UTC 2025

* Automated update of relay data - Sun Nov  2 06:22:16 UTC 2025

* Automated update of relay data - Sun Nov  9 06:21:43 UTC 2025

* Automated update of relay data - Sun Nov 16 06:22:37 UTC 2025

* Automated update of relay data - Sun Nov 23 06:22:51 UTC 2025

* Automated update of relay data - Sun Nov 30 06:24:08 UTC 2025

* Automated update of relay data - Sun Dec  7 06:22:59 UTC 2025

* Automated update of relay data - Sun Dec 14 06:24:33 UTC 2025

* Automated update of relay data - Sun Dec 21 06:24:49 UTC 2025

* Automated update of relay data - Sun Dec 28 06:25:38 UTC 2025

* feat: Add ZXing dependency for QR code scanning

* feat: Request camera permission for QR verification

* Add QR verification payloads and mesh wiring

* Wire verification state, system messages, and notifications

* Add verification sheets and UI affordances

* Show verified badges in sidebar and add strings

* Persist fingerprint caches for offline verification

* Handle bitchat://verify deep links

* feat: Replace zxing-android-embedded with ML Kit and CameraX

* Refactor(Verification): Replace zxing with MLKit for QR scanning

* Replace `AndroidView` with `CameraXViewfinder` for camera preview

* Refactor QR verification: Extract VerificationHandler and fix concurrency issues

* Extract and translate strings for QR verification feature

* Fix build errors: Escape ampersands in strings and restore missing methods in ChatViewModel

* return to main

* return to main 2

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
yet300
2026-01-04 16:29:07 +07:00
committed by GitHub
co-authored by GitHub Action callebtc
parent d73976537d
commit c663e8ede0
56 changed files with 3421 additions and 58 deletions
+9
View File
@@ -90,6 +90,15 @@ dependencies {
// Permissions
implementation(libs.accompanist.permissions)
// QR
implementation(libs.zxing.core)
implementation(libs.mlkit.barcode.scanning)
// CameraX
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.compose)
// Cryptography
implementation(libs.bundles.cryptography)
+9
View File
@@ -31,6 +31,8 @@
<!-- Microphone for voice notes -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- Camera for QR verification -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- Storage permissions for file sharing -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
@@ -47,6 +49,7 @@
<!-- Hardware features -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<uses-feature android:name="android.hardware.bluetooth" android:required="true" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<permission
android:name="com.bitchat.android.permission.FORCE_FINISH"
@@ -89,6 +92,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitchat" android:host="verify" />
</intent-filter>
</activity>
<!-- Persistent foreground service to run the mesh in background -->
@@ -40,6 +40,7 @@ import com.bitchat.android.ui.ChatViewModel
import com.bitchat.android.ui.OrientationAwareActivity
import com.bitchat.android.ui.theme.BitchatTheme
import com.bitchat.android.nostr.PoWPreferenceManager
import com.bitchat.android.services.VerificationService
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -655,6 +656,7 @@ class MainActivity : OrientationAwareActivity() {
// Handle any notification intent
handleNotificationIntent(intent)
handleVerificationIntent(intent)
// Small delay to ensure mesh service is fully initialized
delay(500)
@@ -683,6 +685,7 @@ class MainActivity : OrientationAwareActivity() {
// Handle notification intents when app is already running
if (mainViewModel.onboardingState.value == OnboardingState.COMPLETE) {
handleNotificationIntent(intent)
handleVerificationIntent(intent)
}
}
@@ -788,6 +791,17 @@ class MainActivity : OrientationAwareActivity() {
}
}
private fun handleVerificationIntent(intent: Intent) {
val uri = intent.data ?: return
if (uri.scheme != "bitchat" || uri.host != "verify") return
chatViewModel.showVerificationSheet()
val qr = VerificationService.verifyScannedQR(uri.toString())
if (qr != null) {
chatViewModel.beginQRVerification(qr)
}
}
override fun onDestroy() {
super.onDestroy()
@@ -5,7 +5,10 @@ import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import java.security.MessageDigest
import android.util.Base64
import android.util.Log
import com.bitchat.android.util.hexEncodedString
import androidx.core.content.edit
/**
* Manages persistent identity storage and peer ID rotation - 100% compatible with iOS implementation
@@ -24,9 +27,15 @@ class SecureIdentityStateManager(private val context: Context) {
private const val KEY_STATIC_PUBLIC_KEY = "static_public_key"
private const val KEY_SIGNING_PRIVATE_KEY = "signing_private_key"
private const val KEY_SIGNING_PUBLIC_KEY = "signing_public_key"
private const val KEY_VERIFIED_FINGERPRINTS = "verified_fingerprints"
private const val KEY_CACHED_PEER_FINGERPRINTS = "cached_peer_fingerprints"
private const val KEY_CACHED_PEER_NOISE_KEYS = "cached_peer_noise_keys"
private const val KEY_CACHED_NOISE_FINGERPRINTS = "cached_noise_fingerprints"
private const val KEY_CACHED_FINGERPRINT_NICKNAMES = "cached_fingerprint_nicknames"
}
private val prefs: SharedPreferences
private val lock = Any()
init {
// Create master key for encryption
@@ -168,7 +177,7 @@ class SecureIdentityStateManager(private val context: Context) {
fun generateFingerprint(publicKeyData: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(publicKeyData)
return hash.joinToString("") { "%02x".format(it) }
return hash.hexEncodedString()
}
/**
@@ -178,6 +187,112 @@ class SecureIdentityStateManager(private val context: Context) {
// SHA-256 fingerprint should be 64 hex characters
return fingerprint.matches(Regex("^[a-fA-F0-9]{64}$"))
}
// MARK: - Verified Fingerprints
fun getVerifiedFingerprints(): Set<String> {
return prefs.getStringSet(KEY_VERIFIED_FINGERPRINTS, emptySet())?.toSet() ?: emptySet()
}
fun isVerifiedFingerprint(fingerprint: String): Boolean {
return getVerifiedFingerprints().contains(fingerprint)
}
fun setVerifiedFingerprint(fingerprint: String, verified: Boolean) {
if (!isValidFingerprint(fingerprint)) return
synchronized(lock) {
val current = prefs.getStringSet(KEY_VERIFIED_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
if (verified) {
current.add(fingerprint)
} else {
current.remove(fingerprint)
}
prefs.edit { putStringSet(KEY_VERIFIED_FINGERPRINTS, current) }
}
}
fun getCachedPeerFingerprint(peerID: String): String? {
val pid = peerID.lowercase()
// Reading is safe without lock for SharedPreferences, but synchronizing ensures memory visibility
// if we are paranoid, but SharedPreferences is generally thread-safe for reads.
// However, to ensure we don't read a partial update (unlikely with SP), we can leave it.
// The critical part is the write.
val entries = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet()) ?: return null
val entry = entries.firstOrNull { it.startsWith("$pid:") } ?: return null
return entry.substringAfter(':').takeIf { isValidFingerprint(it) }
}
fun cachePeerFingerprint(peerID: String, fingerprint: String) {
if (!isValidFingerprint(fingerprint)) return
val pid = peerID.lowercase()
synchronized(lock) {
val current = prefs.getStringSet(KEY_CACHED_PEER_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
current.removeAll { it.startsWith("$pid:") }
current.add("$pid:$fingerprint")
prefs.edit { putStringSet(KEY_CACHED_PEER_FINGERPRINTS, current) }
}
}
fun getCachedNoiseKey(peerID: String): String? {
val pid = peerID.lowercase()
val entries = prefs.getStringSet(KEY_CACHED_PEER_NOISE_KEYS, emptySet()) ?: return null
val entry = entries.firstOrNull { it.startsWith("$pid=") } ?: return null
return entry.substringAfter('=').takeIf { it.matches(Regex("^[a-fA-F0-9]{64}$")) }
}
fun cachePeerNoiseKey(peerID: String, noiseKeyHex: String) {
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
val pid = peerID.lowercase()
synchronized(lock) {
val current = prefs.getStringSet(KEY_CACHED_PEER_NOISE_KEYS, emptySet())?.toMutableSet() ?: mutableSetOf()
current.removeAll { it.startsWith("$pid=") }
current.add("$pid=${noiseKeyHex.lowercase()}")
prefs.edit { putStringSet(KEY_CACHED_PEER_NOISE_KEYS, current) }
}
}
fun getCachedNoiseFingerprint(noiseKeyHex: String): String? {
val key = noiseKeyHex.lowercase()
val entries = prefs.getStringSet(KEY_CACHED_NOISE_FINGERPRINTS, emptySet()) ?: return null
val entry = entries.firstOrNull { it.startsWith("$key=") } ?: return null
return entry.substringAfter('=').takeIf { isValidFingerprint(it) }
}
fun cacheNoiseFingerprint(noiseKeyHex: String, fingerprint: String) {
if (!isValidFingerprint(fingerprint)) return
if (!noiseKeyHex.matches(Regex("^[a-fA-F0-9]{64}$"))) return
val key = noiseKeyHex.lowercase()
synchronized(lock) {
val current = prefs.getStringSet(KEY_CACHED_NOISE_FINGERPRINTS, emptySet())?.toMutableSet() ?: mutableSetOf()
current.removeAll { it.startsWith("$key=") }
current.add("$key=$fingerprint")
prefs.edit { putStringSet(KEY_CACHED_NOISE_FINGERPRINTS, current) }
}
}
fun getCachedFingerprintNickname(fingerprint: String): String? {
if (!isValidFingerprint(fingerprint)) return null
val key = fingerprint.lowercase()
val entries = prefs.getStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, emptySet()) ?: return null
val entry = entries.firstOrNull { it.startsWith("$key=") } ?: return null
val encoded = entry.substringAfter('=')
return runCatching {
val bytes = Base64.decode(encoded, Base64.NO_WRAP)
String(bytes, Charsets.UTF_8)
}.getOrNull()
}
fun cacheFingerprintNickname(fingerprint: String, nickname: String) {
if (!isValidFingerprint(fingerprint)) return
val key = fingerprint.lowercase()
val encoded = Base64.encodeToString(nickname.toByteArray(Charsets.UTF_8), Base64.NO_WRAP)
synchronized(lock) {
val current = prefs.getStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, emptySet())?.toMutableSet() ?: mutableSetOf()
current.removeAll { it.startsWith("$key=") }
current.add("$key=$encoded")
prefs.edit { putStringSet(KEY_CACHED_FINGERPRINT_NICKNAMES, current) }
}
}
// MARK: - Peer ID Rotation Management (removed)
// Android now derives peer ID from the persisted Noise identity fingerprint.
@@ -7,12 +7,15 @@ import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.protocol.MessagePadding
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.model.IdentityAnnouncement
import com.bitchat.android.model.NoisePayload
import com.bitchat.android.model.NoisePayloadType
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.protocol.SpecialRecipients
import com.bitchat.android.model.RequestSyncPacket
import com.bitchat.android.sync.GossipSyncManager
import com.bitchat.android.util.toHexString
import com.bitchat.android.services.VerificationService
import kotlinx.coroutines.*
import java.util.*
import kotlin.math.sign
@@ -72,6 +75,7 @@ class BluetoothMeshService(private val context: Context) {
init {
Log.i(TAG, "Initializing BluetoothMeshService for peer=$myPeerID")
VerificationService.configure(encryptionService)
setupDelegates()
messageHandler.packetProcessor = packetProcessor
//startPeriodicDebugLogging()
@@ -414,6 +418,14 @@ class BluetoothMeshService(private val context: Context) {
override fun onReadReceiptReceived(messageID: String, peerID: String) {
delegate?.didReceiveReadReceipt(messageID, peerID)
}
override fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveVerifyChallenge(peerID, payload, timestampMs)
}
override fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long) {
delegate?.didReceiveVerifyResponse(peerID, payload, timestampMs)
}
}
// PacketProcessor delegates
@@ -939,6 +951,50 @@ class BluetoothMeshService(private val context: Context) {
}
}
}
// MARK: QR Verification over Noise
fun sendVerifyChallenge(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
val tlv = VerificationService.buildVerifyChallenge(noiseKeyHex, nonceA)
val payload = NoisePayload(
type = NoisePayloadType.VERIFY_CHALLENGE,
data = tlv
)
sendNoisePayloadToPeer(payload, peerID, "verify challenge")
}
fun sendVerifyResponse(peerID: String, noiseKeyHex: String, nonceA: ByteArray) {
val tlv = VerificationService.buildVerifyResponse(noiseKeyHex, nonceA) ?: return
val payload = NoisePayload(
type = NoisePayloadType.VERIFY_RESPONSE,
data = tlv
)
sendNoisePayloadToPeer(payload, peerID, "verify response")
}
private fun sendNoisePayloadToPeer(payload: NoisePayload, recipientPeerID: String, label: String) {
serviceScope.launch {
try {
val encrypted = encryptionService.encrypt(payload.encode(), recipientPeerID)
val packet = BitchatPacket(
version = 1u,
type = MessageType.NOISE_ENCRYPTED.value,
senderID = hexStringToByteArray(myPeerID),
recipientID = hexStringToByteArray(recipientPeerID),
timestamp = System.currentTimeMillis().toULong(),
payload = encrypted,
signature = null,
ttl = com.bitchat.android.util.AppConstants.MESSAGE_TTL_HOPS
)
val signedPacket = signPacketBeforeBroadcast(packet)
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
Log.d(TAG, "📤 Sent $label to $recipientPeerID (${payload.data.size} bytes)")
} catch (e: Exception) {
Log.e(TAG, "Failed to send $label to $recipientPeerID: ${e.message}")
}
}
}
/**
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
@@ -1127,6 +1183,10 @@ class BluetoothMeshService(private val context: Context) {
fun getIdentityFingerprint(): String {
return encryptionService.getIdentityFingerprint()
}
fun getStaticNoisePublicKey(): ByteArray? {
return encryptionService.getStaticPublicKey()
}
/**
* Check if encryption icon should be shown for a peer
@@ -1283,6 +1343,8 @@ interface BluetoothMeshDelegate {
fun didReceiveChannelLeave(channel: String, fromPeer: String)
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long)
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long)
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
fun getNickname(): String?
fun isFavorite(peerID: String): Boolean
@@ -157,6 +157,14 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro
// Simplified: Call delegate with messageID and peerID directly
delegate?.onReadReceiptReceived(messageID, peerID)
}
com.bitchat.android.model.NoisePayloadType.VERIFY_CHALLENGE -> {
Log.d(TAG, "🔐 Verify challenge received from $peerID (${noisePayload.data.size} bytes)")
delegate?.onVerifyChallengeReceived(peerID, noisePayload.data, packet.timestamp.toLong())
}
com.bitchat.android.model.NoisePayloadType.VERIFY_RESPONSE -> {
Log.d(TAG, "🔐 Verify response received from $peerID (${noisePayload.data.size} bytes)")
delegate?.onVerifyResponseReceived(peerID, noisePayload.data, packet.timestamp.toLong())
}
}
} catch (e: Exception) {
@@ -611,4 +619,6 @@ interface MessageHandlerDelegate {
fun onChannelLeave(channel: String, fromPeer: String)
fun onDeliveryAckReceived(messageID: String, peerID: String)
fun onReadReceiptReceived(messageID: String, peerID: String)
fun onVerifyChallengeReceived(peerID: String, payload: ByteArray, timestampMs: Long)
fun onVerifyResponseReceived(peerID: String, payload: ByteArray, timestampMs: Long)
}
@@ -21,6 +21,8 @@ enum class NoisePayloadType(val value: UByte) {
PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding
READ_RECEIPT(0x02u), // Message was read
DELIVERED(0x03u), // Message was delivered
VERIFY_CHALLENGE(0x10u), // Verification challenge
VERIFY_RESPONSE(0x11u), // Verification response
FILE_TRANSFER(0x20u);
@@ -2,8 +2,12 @@ package com.bitchat.android.nostr
import android.app.Application
import android.util.Log
import com.bitchat.android.model.BitchatFilePacket
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.model.NoisePayload
import com.bitchat.android.model.NoisePayloadType
import com.bitchat.android.model.PrivateMessagePacket
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.services.SeenMessageStore
import com.bitchat.android.ui.ChatState
@@ -71,7 +75,7 @@ class NostrDirectMessageHandler(
if (packet.type != com.bitchat.android.protocol.MessageType.NOISE_ENCRYPTED.value) return@launch
val noisePayload = com.bitchat.android.model.NoisePayload.decode(packet.payload) ?: return@launch
val noisePayload = NoisePayload.decode(packet.payload) ?: return@launch
val messageTimestamp = Date(giftWrap.createdAt * 1000L)
val convKey = "nostr_${senderPubkey.take(16)}"
repo.putNostrKeyMapping(convKey, senderPubkey)
@@ -104,7 +108,7 @@ class NostrDirectMessageHandler(
}
private suspend fun processNoisePayload(
payload: com.bitchat.android.model.NoisePayload,
payload: NoisePayload,
convKey: String,
senderNickname: String,
timestamp: Date,
@@ -112,8 +116,8 @@ class NostrDirectMessageHandler(
recipientIdentity: NostrIdentity
) {
when (payload.type) {
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
val pm = com.bitchat.android.model.PrivateMessagePacket.decode(payload.data) ?: return
NoisePayloadType.PRIVATE_MESSAGE -> {
val pm = PrivateMessagePacket.decode(payload.data) ?: return
val existingMessages = state.getPrivateChatsValue()[convKey] ?: emptyList()
if (existingMessages.any { it.id == pm.messageID }) return
@@ -148,21 +152,21 @@ class NostrDirectMessageHandler(
seenStore.markRead(pm.messageID)
}
}
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
NoisePayloadType.DELIVERED -> {
val messageId = String(payload.data, Charsets.UTF_8)
withContext(Dispatchers.Main) {
meshDelegateHandler.didReceiveDeliveryAck(messageId, convKey)
}
}
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
NoisePayloadType.READ_RECEIPT -> {
val messageId = String(payload.data, Charsets.UTF_8)
withContext(Dispatchers.Main) {
meshDelegateHandler.didReceiveReadReceipt(messageId, convKey)
}
}
com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER -> {
NoisePayloadType.FILE_TRANSFER -> {
// Properly handle encrypted file transfer
val file = com.bitchat.android.model.BitchatFilePacket.decode(payload.data)
val file = BitchatFilePacket.decode(payload.data)
if (file != null) {
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(application, file)
@@ -185,6 +189,8 @@ class NostrDirectMessageHandler(
Log.w(TAG, "⚠️ Failed to decode Nostr file transfer from $convKey")
}
}
NoisePayloadType.VERIFY_CHALLENGE,
NoisePayloadType.VERIFY_RESPONSE -> Unit // Ignore verification payloads in Nostr direct messages
}
}
@@ -0,0 +1,294 @@
package com.bitchat.android.services
import android.net.Uri
import android.util.Base64
import com.bitchat.android.crypto.EncryptionService
import com.bitchat.android.util.AppConstants
import com.bitchat.android.util.dataFromHexString
import com.bitchat.android.util.hexEncodedString
import java.io.ByteArrayOutputStream
import java.security.SecureRandom
import androidx.core.net.toUri
import java.lang.ref.WeakReference
/**
* QR verification helpers: schema, signing, and basic challenge/response helpers.
*/
object VerificationService {
private const val CONTEXT = "bitchat-verify-v1"
private const val RESPONSE_CONTEXT = "bitchat-verify-resp-v1"
private var encryptionServiceRef: WeakReference<EncryptionService>? = null
fun configure(encryptionService: EncryptionService) {
this.encryptionServiceRef = WeakReference(encryptionService)
}
data class VerificationQR(
val v: Int,
val noiseKeyHex: String,
val signKeyHex: String,
val npub: String?,
val nickname: String,
val ts: Long,
val nonceB64: String,
val sigHex: String
) {
fun canonicalBytes(): ByteArray {
val out = ByteArrayOutputStream()
fun appendField(value: String) {
val data = value.toByteArray(Charsets.UTF_8)
val len = minOf(data.size, 255)
out.write(len)
out.write(data, 0, len)
}
appendField(CONTEXT)
appendField(v.toString())
appendField(noiseKeyHex.lowercase())
appendField(signKeyHex.lowercase())
appendField(npub ?: "")
appendField(nickname)
appendField(ts.toString())
appendField(nonceB64)
return out.toByteArray()
}
fun toUrlString(): String {
val builder = Uri.Builder()
.scheme("bitchat")
.authority("verify")
.appendQueryParameter("v", v.toString())
.appendQueryParameter("noise", noiseKeyHex)
.appendQueryParameter("sign", signKeyHex)
.appendQueryParameter("nick", nickname)
.appendQueryParameter("ts", ts.toString())
.appendQueryParameter("nonce", nonceB64)
.appendQueryParameter("sig", sigHex)
if (npub != null) {
builder.appendQueryParameter("npub", npub)
}
return builder.build().toString()
}
companion object {
fun fromUrlString(urlString: String): VerificationQR? {
val uri = runCatching { urlString.toUri() }.getOrNull() ?: return null
if (uri.scheme != "bitchat" || uri.host != "verify") return null
val vStr = uri.getQueryParameter("v") ?: return null
val v = vStr.toIntOrNull() ?: return null
val noise = uri.getQueryParameter("noise") ?: return null
val sign = uri.getQueryParameter("sign") ?: return null
val nick = uri.getQueryParameter("nick") ?: return null
val tsStr = uri.getQueryParameter("ts") ?: return null
val ts = tsStr.toLongOrNull() ?: return null
val nonce = uri.getQueryParameter("nonce") ?: return null
val sig = uri.getQueryParameter("sig") ?: return null
val npub = uri.getQueryParameter("npub")
return VerificationQR(
v = v,
noiseKeyHex = noise,
signKeyHex = sign,
npub = npub,
nickname = nick,
ts = ts,
nonceB64 = nonce,
sigHex = sig
)
}
}
}
fun buildMyQRString(nickname: String, npub: String?): String? {
val service = encryptionServiceRef?.get() ?: return null
val cache = Cache.last
if (cache != null && cache.nickname == nickname && cache.npub == npub) {
if (System.currentTimeMillis() - cache.builtAtMs < 60_000L) {
return cache.value
}
}
val noiseKey = service.getStaticPublicKey()?.hexEncodedString() ?: return null
val signKey = service.getSigningPublicKey()?.hexEncodedString() ?: return null
val ts = System.currentTimeMillis() / 1000L
val nonce = ByteArray(16)
SecureRandom().nextBytes(nonce)
val nonceB64 = Base64.encodeToString(
nonce,
Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
)
val payload = VerificationQR(
v = 1,
noiseKeyHex = noiseKey,
signKeyHex = signKey,
npub = npub,
nickname = nickname,
ts = ts,
nonceB64 = nonceB64,
sigHex = ""
)
val signature = service.signData(payload.canonicalBytes()) ?: return null
val signed = payload.copy(sigHex = signature.hexEncodedString())
val out = signed.toUrlString()
Cache.last = CacheEntry(nickname, npub, System.currentTimeMillis(), out)
return out
}
fun verifyScannedQR(
urlString: String,
maxAgeSeconds: Long = AppConstants.Verification.QR_MAX_AGE_SECONDS
): VerificationQR? {
val service = encryptionServiceRef?.get() ?: return null
val qr = VerificationQR.fromUrlString(urlString) ?: return null
val now = System.currentTimeMillis() / 1000L
if (now - qr.ts > maxAgeSeconds) return null
val sig = qr.sigHex.dataFromHexString() ?: return null
val signKey = qr.signKeyHex.dataFromHexString() ?: return null
val ok = service.verifyEd25519Signature(sig, qr.canonicalBytes(), signKey)
return if (ok) qr else null
}
fun buildVerifyChallenge(noiseKeyHex: String, nonceA: ByteArray): ByteArray {
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
val out = ByteArrayOutputStream()
out.write(0x01)
out.write(minOf(noiseData.size, 255))
out.write(noiseData, 0, minOf(noiseData.size, 255))
out.write(0x02)
out.write(minOf(nonceA.size, 255))
out.write(nonceA, 0, minOf(nonceA.size, 255))
return out.toByteArray()
}
fun buildVerifyResponse(noiseKeyHex: String, nonceA: ByteArray): ByteArray? {
val service = encryptionServiceRef?.get() ?: return null
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
val msg = ByteArrayOutputStream()
msg.write(RESPONSE_CONTEXT.toByteArray(Charsets.UTF_8))
msg.write(minOf(noiseData.size, 255))
msg.write(noiseData, 0, minOf(noiseData.size, 255))
msg.write(nonceA)
val sig = service.signData(msg.toByteArray()) ?: return null
val out = ByteArrayOutputStream()
out.write(0x01)
out.write(minOf(noiseData.size, 255))
out.write(noiseData, 0, minOf(noiseData.size, 255))
out.write(0x02)
out.write(minOf(nonceA.size, 255))
out.write(nonceA, 0, minOf(nonceA.size, 255))
out.write(0x03)
out.write(minOf(sig.size, 255))
out.write(sig, 0, minOf(sig.size, 255))
return out.toByteArray()
}
fun parseVerifyChallenge(data: ByteArray): Pair<String, ByteArray>? {
var idx = 0
fun take(n: Int): ByteArray? {
if (idx + n > data.size) return null
val out = data.copyOfRange(idx, idx + n)
idx += n
return out
}
val t1 = take(1) ?: return null
if (t1[0].toInt() != 0x01) return null
val l1 = take(1)?.get(0)?.toInt() ?: return null
val noiseBytes = take(l1) ?: return null
val noise = noiseBytes.toString(Charsets.UTF_8)
val t2 = take(1) ?: return null
if (t2[0].toInt() != 0x02) return null
val l2 = take(1)?.get(0)?.toInt() ?: return null
val nonce = take(l2) ?: return null
return noise to nonce
}
data class VerifyResponse(val noiseKeyHex: String, val nonceA: ByteArray, val signature: ByteArray) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as VerifyResponse
if (noiseKeyHex != other.noiseKeyHex) return false
if (!nonceA.contentEquals(other.nonceA)) return false
if (!signature.contentEquals(other.signature)) return false
return true
}
override fun hashCode(): Int {
var result = noiseKeyHex.hashCode()
result = 31 * result + nonceA.contentHashCode()
result = 31 * result + signature.contentHashCode()
return result
}
}
fun parseVerifyResponse(data: ByteArray): VerifyResponse? {
var idx = 0
fun take(n: Int): ByteArray? {
if (idx + n > data.size) return null
val out = data.copyOfRange(idx, idx + n)
idx += n
return out
}
val t1 = take(1) ?: return null
if (t1[0].toInt() != 0x01) return null
val l1 = take(1)?.get(0)?.toInt() ?: return null
val noiseBytes = take(l1) ?: return null
val noise = noiseBytes.toString(Charsets.UTF_8)
val t2 = take(1) ?: return null
if (t2[0].toInt() != 0x02) return null
val l2 = take(1)?.get(0)?.toInt() ?: return null
val nonce = take(l2) ?: return null
val t3 = take(1) ?: return null
if (t3[0].toInt() != 0x03) return null
val l3 = take(1)?.get(0)?.toInt() ?: return null
val sig = take(l3) ?: return null
return VerifyResponse(noise, nonce, sig)
}
fun verifyResponseSignature(
noiseKeyHex: String,
nonceA: ByteArray,
signature: ByteArray,
signerPublicKeyHex: String
): Boolean {
val service = encryptionServiceRef?.get() ?: return false
val noiseData = noiseKeyHex.toByteArray(Charsets.UTF_8)
val msg = ByteArrayOutputStream()
msg.write(RESPONSE_CONTEXT.toByteArray(Charsets.UTF_8))
msg.write(minOf(noiseData.size, 255))
msg.write(noiseData, 0, minOf(noiseData.size, 255))
msg.write(nonceA)
val signerKey = signerPublicKeyHex.dataFromHexString() ?: return false
return service.verifyEd25519Signature(signature, msg.toByteArray(), signerKey)
}
private data class CacheEntry(
val nickname: String,
val npub: String?,
val builtAtMs: Long,
val value: String
)
private object Cache {
var last: CacheEntry? = null
}
}
@@ -9,6 +9,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Warning
@@ -681,6 +682,27 @@ fun AboutSheet(
}
}
@Composable
fun CloseButton(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
IconButton(
onClick = onClick,
modifier = modifier
.size(32.dp),
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
containerColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.1f)
)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Close",
modifier = Modifier.size(18.dp)
)
}
}
/**
* Password prompt dialog for password-protected channels
* Kept as dialog since it requires user input
@@ -320,6 +320,10 @@ private fun PrivateChatHeader(
) {
val colorScheme = MaterialTheme.colorScheme
val isNostrDM = peerID.startsWith("nostr_") || peerID.startsWith("nostr:")
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
val isVerified = remember(peerID, verifiedFingerprints) {
viewModel.isPeerVerified(peerID, verifiedFingerprints)
}
// Determine mutual favorite state for this peer (supports mesh ephemeral 16-hex via favorites lookup)
val isMutualFavorite = remember(peerID, peerNicknames) {
try {
@@ -417,17 +421,33 @@ private fun PrivateChatHeader(
// Show a globe when chatting via Nostr alias, or when mesh session not established but mutual favorite exists
val showGlobe = isNostrDM || (sessionState != "established" && isMutualFavorite)
val securityModifier = if (!isNostrDM) {
Modifier.clickable { viewModel.showSecurityVerificationSheet() }
} else {
Modifier
}
if (showGlobe) {
Icon(
imageVector = Icons.Outlined.Public,
contentDescription = stringResource(R.string.cd_nostr_reachable),
modifier = Modifier.size(14.dp),
modifier = Modifier.size(14.dp).then(securityModifier),
tint = Color(0xFF9B59B6) // Purple like iOS
)
} else {
NoiseSessionIcon(
sessionState = sessionState,
modifier = Modifier.size(14.dp)
modifier = Modifier.size(14.dp).then(securityModifier)
)
}
if (isVerified) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Filled.Verified,
contentDescription = stringResource(R.string.verify_title),
modifier = Modifier.size(14.dp).then(securityModifier),
tint = Color(0xFF32D74B)
)
}
@@ -58,6 +58,8 @@ fun ChatScreen(viewModel: ChatViewModel) {
val showMentionSuggestions by viewModel.showMentionSuggestions.collectAsStateWithLifecycle()
val mentionSuggestions by viewModel.mentionSuggestions.collectAsStateWithLifecycle()
val showAppInfo by viewModel.showAppInfo.collectAsStateWithLifecycle()
val showVerificationSheet by viewModel.showVerificationSheet.collectAsStateWithLifecycle()
val showSecurityVerificationSheet by viewModel.showSecurityVerificationSheet.collectAsStateWithLifecycle()
var messageText by remember { mutableStateOf(TextFieldValue("")) }
var showPasswordPrompt by remember { mutableStateOf(false) }
@@ -322,6 +324,10 @@ fun ChatScreen(viewModel: ChatViewModel) {
SidebarOverlay(
viewModel = viewModel,
onDismiss = { viewModel.hideSidebar() },
onShowVerification = {
viewModel.showVerificationSheet(fromSidebar = true)
viewModel.hideSidebar()
},
modifier = Modifier.fillMaxSize()
)
}
@@ -368,7 +374,11 @@ fun ChatScreen(viewModel: ChatViewModel) {
},
selectedUserForSheet = selectedUserForSheet,
selectedMessageForSheet = selectedMessageForSheet,
viewModel = viewModel
viewModel = viewModel,
showVerificationSheet = showVerificationSheet,
onVerificationSheetDismiss = viewModel::hideVerificationSheet,
showSecurityVerificationSheet = showSecurityVerificationSheet,
onSecurityVerificationSheetDismiss = viewModel::hideSecurityVerificationSheet
)
}
@@ -516,7 +526,11 @@ private fun ChatDialogs(
onUserSheetDismiss: () -> Unit,
selectedUserForSheet: String,
selectedMessageForSheet: BitchatMessage?,
viewModel: ChatViewModel
viewModel: ChatViewModel,
showVerificationSheet: Boolean,
onVerificationSheetDismiss: () -> Unit,
showSecurityVerificationSheet: Boolean,
onSecurityVerificationSheetDismiss: () -> Unit
) {
// Password dialog
PasswordPromptDialog(
@@ -570,4 +584,20 @@ private fun ChatDialogs(
viewModel = viewModel
)
}
if (showVerificationSheet) {
VerificationSheet(
isPresented = showVerificationSheet,
onDismiss = onVerificationSheetDismiss,
viewModel = viewModel
)
}
if (showSecurityVerificationSheet) {
SecurityVerificationSheet(
isPresented = showSecurityVerificationSheet,
onDismiss = onSecurityVerificationSheetDismiss,
viewModel = viewModel
)
}
}
@@ -122,6 +122,12 @@ class ChatState(
// Navigation state
private val _showAppInfo = MutableStateFlow<Boolean>(false)
val showAppInfo: StateFlow<Boolean> = _showAppInfo.asStateFlow()
private val _showVerificationSheet = MutableStateFlow(false)
val showVerificationSheet: StateFlow<Boolean> = _showVerificationSheet.asStateFlow()
private val _showSecurityVerificationSheet = MutableStateFlow(false)
val showSecurityVerificationSheet: StateFlow<Boolean> = _showSecurityVerificationSheet.asStateFlow()
// Location channels state (for Nostr geohash features)
private val _selectedLocationChannel = MutableStateFlow<com.bitchat.android.geohash.ChannelID?>(com.bitchat.android.geohash.ChannelID.Mesh)
@@ -302,6 +308,14 @@ class ChatState(
fun setShowAppInfo(show: Boolean) {
_showAppInfo.value = show
}
fun setShowVerificationSheet(show: Boolean) {
_showVerificationSheet.value = show
}
fun setShowSecurityVerificationSheet(show: Boolean) {
_showSecurityVerificationSheet.value = show
}
fun setSelectedLocationChannel(channel: com.bitchat.android.geohash.ChannelID?) {
_selectedLocationChannel.value = channel
@@ -5,11 +5,15 @@ import android.util.Log
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.bitchat.android.favorites.FavoritesPersistenceService
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import com.bitchat.android.mesh.BluetoothMeshDelegate
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.BitchatMessageType
import com.bitchat.android.nostr.NostrIdentityBridge
import com.bitchat.android.protocol.BitchatPacket
@@ -19,6 +23,13 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.Date
import kotlin.random.Random
import com.bitchat.android.services.VerificationService
import com.bitchat.android.identity.SecureIdentityStateManager
import com.bitchat.android.noise.NoiseSession
import com.bitchat.android.nostr.GeohashAliasRegistry
import com.bitchat.android.util.dataFromHexString
import com.bitchat.android.util.hexEncodedString
import java.security.MessageDigest
/**
* Refactored ChatViewModel - Main coordinator for bitchat functionality
@@ -46,6 +57,20 @@ class ChatViewModel(
mediaSendingManager.sendImageNote(toPeerIDOrNull, channelOrNull, filePath)
}
fun getCurrentNpub(): String? {
return try {
NostrIdentityBridge
.getCurrentNostrIdentity(getApplication())
?.npub
} catch (_: Exception) {
null
}
}
fun buildMyQRString(nickname: String, npub: String?): String {
return VerificationService.buildMyQRString(nickname, npub) ?: ""
}
// MARK: - State management
private val state = ChatState(
scope = viewModelScope,
@@ -57,6 +82,7 @@ class ChatViewModel(
// Specialized managers
private val dataManager = DataManager(application.applicationContext)
private val identityManager by lazy { SecureIdentityStateManager(getApplication()) }
private val messageManager = MessageManager(state)
private val channelManager = ChannelManager(state, messageManager, dataManager, viewModelScope)
@@ -75,6 +101,17 @@ class ChatViewModel(
NotificationIntervalManager()
)
private val verificationHandler = VerificationHandler(
context = application.applicationContext,
scope = viewModelScope,
meshService = meshService,
identityManager = identityManager,
state = state,
notificationManager = notificationManager,
messageManager = messageManager
)
val verifiedFingerprints = verificationHandler.verifiedFingerprints
// Media file sending manager
private val mediaSendingManager = MediaSendingManager(state, messageManager, channelManager, meshService)
@@ -134,6 +171,8 @@ class ChatViewModel(
val peerRSSI: StateFlow<Map<String, Int>> = state.peerRSSI
val peerDirect: StateFlow<Map<String, Boolean>> = state.peerDirect
val showAppInfo: StateFlow<Boolean> = state.showAppInfo
val showVerificationSheet: StateFlow<Boolean> = state.showVerificationSheet
val showSecurityVerificationSheet: StateFlow<Boolean> = state.showSecurityVerificationSheet
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = state.selectedLocationChannel
val isTeleported: StateFlow<Boolean> = state.isTeleported
val geohashPeople: StateFlow<List<GeoPerson>> = state.geohashPeople
@@ -247,6 +286,9 @@ class ChatViewModel(
// Initialize favorites persistence service
com.bitchat.android.favorites.FavoritesPersistenceService.initialize(getApplication())
// Load verified fingerprints from secure storage
verificationHandler.loadVerifiedFingerprints()
// Ensure NostrTransport knows our mesh peer ID for embedded packets
try {
@@ -646,6 +688,18 @@ class ChatViewModel(
// Update fingerprint mappings from centralized manager
val fingerprints = privateChatManager.getAllPeerFingerprints()
state.setPeerFingerprints(fingerprints)
fingerprints.forEach { (peerID, fingerprint) ->
identityManager.cachePeerFingerprint(peerID, fingerprint)
val info = try { meshService.getPeerInfo(peerID) } catch (_: Exception) { null }
val noiseKeyHex = info?.noisePublicKey?.hexEncodedString()
if (noiseKeyHex != null) {
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
identityManager.cacheNoiseFingerprint(noiseKeyHex, fingerprint)
}
info?.nickname?.takeIf { it.isNotBlank() }?.let { nickname ->
identityManager.cacheFingerprintNickname(fingerprint, nickname)
}
}
val nicknames = meshService.getPeerNicknames()
state.setPeerNicknames(nicknames)
@@ -660,6 +714,34 @@ class ChatViewModel(
}
state.setPeerDirect(directMap)
} catch (_: Exception) { }
// Flush any pending QR verification once a Noise session is established
currentPeers.forEach { peerID ->
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
verificationHandler.sendPendingVerificationIfNeeded(peerID)
}
}
}
// MARK: - QR Verification
fun isPeerVerified(peerID: String, verifiedFingerprints: Set<String>): Boolean {
if (peerID.startsWith("nostr_") || peerID.startsWith("nostr:")) return false
val fingerprint = verificationHandler.getPeerFingerprintForDisplay(peerID)
return fingerprint != null && verifiedFingerprints.contains(fingerprint)
}
fun isNoisePublicKeyVerified(noisePublicKey: ByteArray, verifiedFingerprints: Set<String>): Boolean {
val fingerprint = verificationHandler.fingerprintFromNoiseBytes(noisePublicKey)
return verifiedFingerprints.contains(fingerprint)
}
fun unverifyFingerprint(peerID: String) {
verificationHandler.unverifyFingerprint(peerID)
}
fun beginQRVerification(qr: VerificationService.VerificationQR): Boolean {
return verificationHandler.beginQRVerification(qr)
}
// MARK: - Debug and Troubleshooting
@@ -668,41 +750,75 @@ class ChatViewModel(
return meshService.getDebugStatus()
}
// Note: Mesh service restart is now handled by MainActivity
// This function is no longer needed
fun setAppBackgroundState(inBackground: Boolean) {
// Forward to notification manager for notification logic
notificationManager.setAppBackgroundState(inBackground)
}
fun setCurrentPrivateChatPeer(peerID: String?) {
// Update notification manager with current private chat peer
notificationManager.setCurrentPrivateChatPeer(peerID)
}
fun setCurrentGeohash(geohash: String?) {
// Update notification manager with current geohash for notification logic
notificationManager.setCurrentGeohash(geohash)
}
fun clearNotificationsForSender(peerID: String) {
// Clear notifications when user opens a chat
notificationManager.clearNotificationsForSender(peerID)
}
fun clearNotificationsForGeohash(geohash: String) {
// Clear notifications when user opens a geohash chat
notificationManager.clearNotificationsForGeohash(geohash)
}
/**
* Clear mesh mention notifications when user opens mesh chat
*/
fun clearMeshMentionNotifications() {
notificationManager.clearMeshMentionNotifications()
}
private var reopenSidebarAfterVerification = false
fun showVerificationSheet(fromSidebar: Boolean = false) {
if (fromSidebar) {
reopenSidebarAfterVerification = true
}
state.setShowVerificationSheet(true)
}
fun hideVerificationSheet() {
state.setShowVerificationSheet(false)
if (reopenSidebarAfterVerification) {
reopenSidebarAfterVerification = false
state.setShowSidebar(true)
}
}
fun showSecurityVerificationSheet() {
state.setShowSecurityVerificationSheet(true)
}
fun hideSecurityVerificationSheet() {
state.setShowSecurityVerificationSheet(false)
}
fun getPeerFingerprintForDisplay(peerID: String): String? {
return verificationHandler.getPeerFingerprintForDisplay(peerID)
}
fun getMyFingerprint(): String {
return verificationHandler.getMyFingerprint()
}
fun resolvePeerDisplayNameForFingerprint(peerID: String): String {
return verificationHandler.resolvePeerDisplayNameForFingerprint(peerID)
}
fun verifyFingerprintValue(fingerprint: String) {
verificationHandler.verifyFingerprintValue(fingerprint)
}
fun unverifyFingerprintValue(fingerprint: String) {
verificationHandler.unverifyFingerprintValue(fingerprint)
}
// MARK: - Command Autocomplete (delegated)
fun updateCommandSuggestions(input: String) {
@@ -744,6 +860,14 @@ class ChatViewModel(
override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) {
meshDelegateHandler.didReceiveReadReceipt(messageID, recipientPeerID)
}
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
verificationHandler.didReceiveVerifyChallenge(peerID, payload)
}
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
verificationHandler.didReceiveVerifyResponse(peerID, payload)
}
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
return meshDelegateHandler.decryptChannelMessage(encryptedContent, channel)
@@ -827,7 +951,7 @@ class ChatViewModel(
// Clear secure identity state (if used)
try {
val identityManager = com.bitchat.android.identity.SecureIdentityStateManager(getApplication())
val identityManager = SecureIdentityStateManager(getApplication())
identityManager.clearIdentityData()
// Also clear secure values used by FavoritesPersistenceService (favorites + peerID index)
try {
@@ -840,7 +964,7 @@ class ChatViewModel(
// Clear FavoritesPersistenceService persistent relationships
try {
com.bitchat.android.favorites.FavoritesPersistenceService.shared.clearAllFavorites()
FavoritesPersistenceService.shared.clearAllFavorites()
Log.d(TAG, "✅ Cleared FavoritesPersistenceService relationships")
} catch (_: Exception) { }
@@ -969,5 +1093,6 @@ class ChatViewModel(
*/
fun colorForNostrPubkey(pubkeyHex: String, isDark: Boolean): androidx.compose.ui.graphics.Color {
return geohashViewModel.colorForNostrPubkey(pubkeyHex, isDark)
}
}
}
@@ -216,6 +216,14 @@ class MeshDelegateHandler(
messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Read(recipientPeerID, Date()))
}
}
override fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray, timestampMs: Long) {
// Handled by ChatViewModel for verification flow
}
override fun didReceiveVerifyResponse(peerID: String, payload: ByteArray, timestampMs: Long) {
// Handled by ChatViewModel for verification flow
}
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
return channelManager.decryptChannelMessage(encryptedContent, channel)
@@ -280,6 +280,37 @@ class NotificationManager(
Log.d(TAG, "Displayed notification for $contentTitle with ID $notificationId")
}
fun showVerificationNotification(title: String, body: String, peerID: String? = null) {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
if (peerID != null) {
putExtra(EXTRA_OPEN_PRIVATE_CHAT, true)
putExtra(EXTRA_PEER_ID, peerID)
putExtra(EXTRA_SENDER_NICKNAME, body)
}
}
val pendingIntent = PendingIntent.getActivity(
context,
(System.currentTimeMillis() and 0x7FFFFFFF).toInt(),
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title)
.setContentText(body)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setShowWhen(true)
.setWhen(System.currentTimeMillis())
notificationManager.notify((System.currentTimeMillis() and 0x7FFFFFFF).toInt(), builder.build())
}
private fun showNotificationForActivePeers(peersSize: Int) {
// Create intent to open the app
val intent = Intent(context, MainActivity::class.java).apply {
@@ -0,0 +1,443 @@
package com.bitchat.android.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Verified
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.NoEncryption
import androidx.compose.material.icons.outlined.Sync
import androidx.compose.material.icons.outlined.Warning as OutlinedWarning
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.R
private data class SecurityStatusInfo(
val text: String,
val icon: ImageVector,
val tint: Color
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SecurityVerificationSheet(
isPresented: Boolean,
onDismiss: () -> Unit,
viewModel: ChatViewModel,
modifier: Modifier = Modifier
) {
if (!isPresented) return
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val peerID by viewModel.selectedPrivateChatPeer.collectAsStateWithLifecycle()
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
val peerSessionStates by viewModel.peerSessionStates.collectAsStateWithLifecycle()
val isDark = isSystemInDarkTheme()
val accent = if (isDark) Color.Green else Color(0xFF008000)
val boxColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.06f)
val peerHexRegex = remember { Regex("^[0-9a-fA-F]{16}$") }
ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.background,
dragHandle = null
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
SecurityVerificationHeader(
accent = accent,
onClose = onDismiss
)
if (peerID == null) {
Text(
text = stringResource(R.string.fingerprint_no_peer),
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
} else {
val selectedPeerID = peerID!!
val displayName = viewModel.resolvePeerDisplayNameForFingerprint(selectedPeerID)
val fingerprint = viewModel.getPeerFingerprintForDisplay(selectedPeerID)
val isVerified = fingerprint != null && verifiedFingerprints.contains(fingerprint)
val sessionState = peerSessionStates[selectedPeerID]
val statusInfo = buildStatusInfo(
isVerified = isVerified,
sessionState = sessionState,
accent = accent
)
SecurityStatusCard(
displayName = displayName,
accent = accent,
boxColor = boxColor,
statusInfo = statusInfo
)
FingerprintBlock(
title = stringResource(R.string.fingerprint_their),
fingerprint = fingerprint,
boxColor = boxColor,
accent = accent
)
FingerprintBlock(
title = stringResource(R.string.fingerprint_yours),
fingerprint = viewModel.getMyFingerprint(),
boxColor = boxColor,
accent = accent
)
SecurityVerificationActions(
isVerified = isVerified,
fingerprint = fingerprint,
displayName = displayName,
accent = accent,
canStartHandshake = fingerprint == null && selectedPeerID.matches(peerHexRegex),
onStartHandshake = { viewModel.meshService.initiateNoiseHandshake(selectedPeerID) },
onVerify = { fp -> viewModel.verifyFingerprintValue(fp) },
onUnverify = { fp -> viewModel.unverifyFingerprintValue(fp) }
)
}
}
}
}
@Composable
private fun SecurityVerificationHeader(
accent: Color,
onClose: () -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.security_verification_title),
style = MaterialTheme.typography.titleSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
),
color = accent
)
Spacer(modifier = Modifier.weight(1f))
CloseButton(onClick = onClose)
}
}
@Composable
private fun buildStatusInfo(
isVerified: Boolean,
sessionState: String?,
accent: Color
): SecurityStatusInfo {
val text = when {
isVerified -> stringResource(R.string.fingerprint_status_verified)
sessionState == "established" -> stringResource(R.string.fingerprint_status_encrypted)
sessionState == "handshaking" -> stringResource(R.string.fingerprint_status_handshaking)
sessionState == "failed" -> stringResource(R.string.fingerprint_status_failed)
else -> stringResource(R.string.fingerprint_status_uninitialized)
}
val icon = when {
isVerified -> Icons.Filled.Verified
sessionState == "handshaking" -> Icons.Outlined.Sync
sessionState == "failed" -> Icons.Outlined.OutlinedWarning
sessionState == "established" -> Icons.Filled.Lock
else -> Icons.Outlined.NoEncryption
}
val tint = when {
isVerified -> Color(0xFF32D74B)
sessionState == "failed" -> Color(0xFFFF3B30)
sessionState == "handshaking" -> Color(0xFFFF9500)
sessionState == "established" -> Color(0xFF32D74B)
else -> accent.copy(alpha = 0.6f)
}
return SecurityStatusInfo(text, icon, tint)
}
@Composable
private fun SecurityStatusCard(
displayName: String,
accent: Color,
boxColor: Color,
statusInfo: SecurityStatusInfo
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(boxColor, shape = MaterialTheme.shapes.medium)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = statusInfo.icon,
contentDescription = null,
tint = statusInfo.tint
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = displayName,
style = MaterialTheme.typography.titleMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
),
color = accent
)
Text(
text = statusInfo.text,
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace
),
color = accent.copy(alpha = 0.8f)
)
}
}
}
@Composable
private fun SecurityVerificationActions(
isVerified: Boolean,
fingerprint: String?,
displayName: String,
accent: Color,
canStartHandshake: Boolean,
onStartHandshake: () -> Unit,
onVerify: (String) -> Unit,
onUnverify: (String) -> Unit
) {
if (canStartHandshake) {
Button(
onClick = onStartHandshake,
colors = ButtonDefaults.buttonColors(
containerColor = accent,
contentColor = Color.White
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.fingerprint_start_handshake),
fontFamily = FontFamily.Monospace,
fontSize = 12.sp
)
}
}
if (isVerified) {
VerificationStatusRow(
icon = Icons.Filled.Verified,
iconTint = Color(0xFF32D74B),
text = stringResource(R.string.fingerprint_verified_label),
textTint = Color(0xFF32D74B)
)
Text(
text = stringResource(R.string.fingerprint_verified_message),
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace
),
color = accent.copy(alpha = 0.7f),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
Button(
onClick = { fingerprint?.let(onUnverify) },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFFFF3B30),
contentColor = Color.White
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.verify_remove),
fontFamily = FontFamily.Monospace,
fontSize = 12.sp
)
}
} else {
VerificationStatusRow(
icon = Icons.Filled.Warning,
iconTint = Color(0xFFFF9500),
text = stringResource(R.string.fingerprint_not_verified_label),
textTint = Color(0xFFFF9500)
)
Text(
text = stringResource(R.string.fingerprint_not_verified_message_fmt, displayName),
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace
),
color = accent.copy(alpha = 0.7f),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
if (fingerprint != null) {
Button(
onClick = { onVerify(fingerprint) },
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF34C759),
contentColor = Color.White
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.fingerprint_mark_verified),
fontFamily = FontFamily.Monospace,
fontSize = 12.sp
)
}
}
}
}
@Composable
private fun VerificationStatusRow(
icon: ImageVector,
iconTint: Color,
text: String,
textTint: Color
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = iconTint
)
Spacer(modifier = Modifier.width(6.dp))
Text(
text = text,
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
),
color = textTint
)
}
}
@Composable
private fun FingerprintBlock(
title: String,
fingerprint: String?,
boxColor: Color,
accent: Color
) {
val clipboardManager = LocalClipboardManager.current
var showMenu by remember(fingerprint) { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = title,
style = MaterialTheme.typography.labelSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
),
color = accent.copy(alpha = 0.8f)
)
if (fingerprint != null) {
Column {
Text(
text = formatFingerprint(fingerprint),
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontSize = 14.sp
),
color = accent,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
interactionSource = interactionSource,
indication = null,
onClick = {},
onLongClick = { showMenu = true }
)
.background(boxColor, shape = MaterialTheme.shapes.small)
.padding(16.dp),
)
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { Text(text = stringResource(R.string.fingerprint_copy)) },
onClick = {
clipboardManager.setText(AnnotatedString(fingerprint))
showMenu = false
}
)
}
}
} else {
Text(
text = stringResource(R.string.fingerprint_pending),
style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
color = Color(0xFFFF9500),
modifier = Modifier.padding(16.dp)
)
}
}
}
private fun formatFingerprint(fingerprint: String): String {
val upper = fingerprint.uppercase()
val sb = StringBuilder()
upper.forEachIndexed { index, c ->
if (index > 0 && index % 4 == 0) {
if (index % 16 == 0) sb.append('\n') else sb.append(' ')
}
sb.append(c)
}
return sb.toString()
}
@@ -23,6 +23,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.text.style.TextOverflow
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import com.bitchat.android.util.hexEncodedString
/**
@@ -34,6 +35,7 @@ import com.bitchat.android.ui.theme.BASE_FONT_SIZE
fun SidebarOverlay(
viewModel: ChatViewModel,
onDismiss: () -> Unit,
onShowVerification: () -> Unit,
modifier: Modifier = Modifier
) {
val colorScheme = MaterialTheme.colorScheme
@@ -131,6 +133,7 @@ fun SidebarOverlay(
colorScheme = colorScheme,
selectedPrivatePeer = selectedPrivatePeer,
viewModel = viewModel,
onShowVerification = onShowVerification,
onPrivateChatStart = { peerID ->
viewModel.startPrivateChat(peerID)
onDismiss()
@@ -257,8 +260,11 @@ fun PeopleSection(
colorScheme: ColorScheme,
selectedPrivatePeer: String?,
viewModel: ChatViewModel,
onShowVerification: () -> Unit,
onPrivateChatStart: (String) -> Unit
) {
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
Column(modifier = modifier) {
Row(
modifier = Modifier
@@ -279,6 +285,17 @@ fun PeopleSection(
color = colorScheme.onSurface.copy(alpha = 0.6f),
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.weight(1f))
if (selectedLocationChannel !is com.bitchat.android.geohash.ChannelID.Location) {
IconButton(onClick = onShowVerification, modifier = Modifier.size(24.dp)) {
Icon(
imageVector = Icons.Outlined.QrCode,
contentDescription = stringResource(R.string.verify_title),
tint = colorScheme.onSurface.copy(alpha = 0.8f),
modifier = Modifier.size(16.dp)
)
}
}
}
if (connectedPeers.isEmpty()) {
@@ -295,6 +312,7 @@ fun PeopleSection(
val privateChats by viewModel.privateChats.collectAsStateWithLifecycle()
val favoritePeers by viewModel.favoritePeers.collectAsStateWithLifecycle()
val peerFingerprints by viewModel.peerFingerprints.collectAsStateWithLifecycle()
val verifiedFingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
// Reactive favorite computation for all peers
val peerFavoriteStates = remember(favoritePeers, peerFingerprints, connectedPeers) {
@@ -308,10 +326,16 @@ fun PeopleSection(
// Build mapping of connected peerID -> noise key hex to unify with offline favorites
val noiseHexByPeerID: Map<String, String> = connectedPeers.associateWith { pid ->
try {
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
viewModel.meshService.getPeerInfo(pid)?.noisePublicKey?.hexEncodedString()
} catch (_: Exception) { null }
}.filterValues { it != null }.mapValues { it.value!! }
val peerVerifiedStates = remember(verifiedFingerprints, peerFingerprints, connectedPeers) {
connectedPeers.associateWith { peerID ->
viewModel.isPeerVerified(peerID, verifiedFingerprints)
}
}
Log.d("SidebarComponents", "Recomposing with ${favoritePeers.size} favorites, peer states: $peerFavoriteStates")
// Smart sorting: unread DMs first, then by most recent DM, then favorites, then alphabetical
@@ -342,7 +366,7 @@ fun PeopleSection(
// Offline favorites (exclude ones mapped to connected)
val offlineFavorites = com.bitchat.android.favorites.FavoritesPersistenceService.shared.getOurFavorites()
offlineFavorites.forEach { fav ->
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
val favPeerID = fav.peerNoisePublicKey.hexEncodedString()
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
if (!isMappedToConnected) {
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
@@ -368,6 +392,7 @@ fun PeopleSection(
sortedPeers.forEach { peerID ->
val isFavorite = peerFavoriteStates[peerID] ?: false
val isVerified = peerVerifiedStates[peerID] ?: false
// fingerprint and favorite relationship resolution not needed here; UI will show Nostr globe for appended offline favorites below
val noiseHex = noiseHexByPeerID[peerID]
@@ -392,6 +417,7 @@ fun PeopleSection(
isDirect = isDirectLive,
isSelected = peerID == selectedPrivatePeer,
isFavorite = isFavorite,
isVerified = isVerified,
hasUnreadDM = combinedHasUnread,
colorScheme = colorScheme,
viewModel = viewModel,
@@ -408,7 +434,7 @@ fun PeopleSection(
// Append offline favorites we actively favorite (and not currently connected)
offlineFavorites.forEach { fav ->
val favPeerID = fav.peerNoisePublicKey.joinToString("") { b -> "%02x".format(b) }
val favPeerID = fav.peerNoisePublicKey.hexEncodedString()
// If any connected peer maps to this noise key, skip showing the offline entry
val isMappedToConnected = noiseHexByPeerID.values.any { it.equals(favPeerID, ignoreCase = true) }
if (isMappedToConnected) return@forEach
@@ -419,7 +445,7 @@ fun PeopleSection(
if (npubOrHex != null) {
val hex = if (npubOrHex.startsWith("npub")) {
val (hrp, data) = com.bitchat.android.nostr.Bech32.decode(npubOrHex)
if (hrp == "npub") data.joinToString("") { "%02x".format(it) } else null
if (hrp == "npub") data.hexEncodedString() else null
} else {
npubOrHex.lowercase()
}
@@ -435,6 +461,7 @@ fun PeopleSection(
val dn = peerNicknames[favPeerID] ?: fav.peerNickname
val (bName, _) = com.bitchat.android.ui.splitSuffix(dn)
val showHash = (baseNameCounts[bName] ?: 0) > 1
val isVerified = viewModel.isNoisePublicKeyVerified(fav.peerNoisePublicKey, verifiedFingerprints)
// Compute unreadCount from either noise conversation or Nostr conversation
val unreadCount = (
@@ -449,6 +476,7 @@ fun PeopleSection(
isDirect = false,
isSelected = (mappedConnectedPeerID ?: favPeerID) == selectedPrivatePeer,
isFavorite = true,
isVerified = isVerified,
hasUnreadDM = hasUnread,
colorScheme = colorScheme,
viewModel = viewModel,
@@ -516,6 +544,7 @@ private fun PeerItem(
isDirect: Boolean,
isSelected: Boolean,
isFavorite: Boolean,
isVerified: Boolean,
hasUnreadDM: Boolean,
colorScheme: ColorScheme,
viewModel: ChatViewModel,
@@ -616,6 +645,16 @@ private fun PeerItem(
color = baseColor.copy(alpha = 0.6f)
)
}
if (isVerified) {
Spacer(modifier = Modifier.width(4.dp))
Icon(
imageVector = Icons.Filled.Verified,
contentDescription = null,
modifier = Modifier.size(14.dp),
tint = Color(0xFF32D74B)
)
}
}
// Favorite star with proper filled/outlined states
@@ -0,0 +1,368 @@
package com.bitchat.android.ui
import android.content.Context
import com.bitchat.android.R
import com.bitchat.android.favorites.FavoritesPersistenceService
import com.bitchat.android.identity.SecureIdentityStateManager
import com.bitchat.android.mesh.BluetoothMeshService
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.noise.NoiseSession
import com.bitchat.android.nostr.GeohashAliasRegistry
import com.bitchat.android.services.VerificationService
import com.bitchat.android.util.dataFromHexString
import com.bitchat.android.util.hexEncodedString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import java.security.MessageDigest
import java.util.Date
import java.util.concurrent.ConcurrentHashMap
/**
* Handles QR verification logic and state, extracted from ChatViewModel.
*/
class VerificationHandler(
private val context: Context,
private val scope: CoroutineScope,
private val meshService: BluetoothMeshService,
private val identityManager: SecureIdentityStateManager,
private val state: ChatState,
private val notificationManager: NotificationManager,
private val messageManager: MessageManager
) {
private val _verifiedFingerprints = MutableStateFlow<Set<String>>(emptySet())
val verifiedFingerprints: StateFlow<Set<String>> = _verifiedFingerprints.asStateFlow()
private val pendingQRVerifications = ConcurrentHashMap<String, PendingVerification>()
private val lastVerifyNonceByPeer = ConcurrentHashMap<String, ByteArray>()
private val lastInboundVerifyChallengeAt = ConcurrentHashMap<String, Long>()
private val lastMutualToastAt = ConcurrentHashMap<String, Long>()
fun loadVerifiedFingerprints() {
_verifiedFingerprints.value = identityManager.getVerifiedFingerprints()
}
fun isPeerVerified(peerID: String): Boolean {
if (peerID.startsWith("nostr_") || peerID.startsWith("nostr:")) return false
val fingerprint = getPeerFingerprintForDisplay(peerID)
return fingerprint != null && _verifiedFingerprints.value.contains(fingerprint)
}
fun isNoisePublicKeyVerified(noisePublicKey: ByteArray): Boolean {
val fingerprint = fingerprintFromNoiseBytes(noisePublicKey)
return _verifiedFingerprints.value.contains(fingerprint)
}
fun unverifyFingerprint(peerID: String) {
val fingerprint = meshService.getPeerFingerprint(peerID) ?: return
identityManager.setVerifiedFingerprint(fingerprint, false)
val current = _verifiedFingerprints.value.toMutableSet()
current.remove(fingerprint)
_verifiedFingerprints.value = current
}
fun beginQRVerification(qr: VerificationService.VerificationQR): Boolean {
val targetNoise = qr.noiseKeyHex.lowercase()
val peerID = state.getConnectedPeersValue().firstOrNull { pid ->
val noiseKeyHex = meshService.getPeerInfo(pid)?.noisePublicKey?.hexEncodedString()?.lowercase()
noiseKeyHex == targetNoise
} ?: return false
if (pendingQRVerifications.containsKey(peerID)) return true
val nonce = ByteArray(16)
java.security.SecureRandom().nextBytes(nonce)
val pending = PendingVerification(qr.noiseKeyHex, qr.signKeyHex, nonce, System.currentTimeMillis(), false)
pendingQRVerifications[peerID] = pending
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
meshService.sendVerifyChallenge(peerID, qr.noiseKeyHex, nonce)
pendingQRVerifications[peerID] = pending.copy(sent = true)
} else {
meshService.initiateNoiseHandshake(peerID)
}
fingerprintFromNoiseHex(qr.noiseKeyHex)?.let { fp ->
identityManager.cacheFingerprintNickname(fp, qr.nickname)
identityManager.cacheNoiseFingerprint(qr.noiseKeyHex, fp)
identityManager.cachePeerNoiseKey(peerID, qr.noiseKeyHex)
}
return true
}
fun sendPendingVerificationIfNeeded(peerID: String) {
val pending = pendingQRVerifications[peerID] ?: return
if (pending.sent) return
meshService.sendVerifyChallenge(peerID, pending.noiseKeyHex, pending.nonceA)
pendingQRVerifications[peerID] = pending.copy(sent = true)
}
fun didReceiveVerifyChallenge(peerID: String, payload: ByteArray) {
scope.launch {
val parsed = VerificationService.parseVerifyChallenge(payload) ?: return@launch
val myNoiseHex = meshService.getStaticNoisePublicKey()?.hexEncodedString()?.lowercase() ?: return@launch
if (parsed.first.lowercase() != myNoiseHex) return@launch
val lastNonce = lastVerifyNonceByPeer[peerID]
if (lastNonce != null && lastNonce.contentEquals(parsed.second)) return@launch
lastVerifyNonceByPeer[peerID] = parsed.second
val fp = meshService.getPeerFingerprint(peerID)
if (fp != null) {
lastInboundVerifyChallengeAt[fp] = System.currentTimeMillis()
if (_verifiedFingerprints.value.contains(fp)) {
val lastToast = lastMutualToastAt[fp] ?: 0L
if (System.currentTimeMillis() - lastToast > 60_000L) {
lastMutualToastAt[fp] = System.currentTimeMillis()
val name = resolvePeerDisplayName(peerID)
val body = context.getString(R.string.verify_mutual_match_body, name)
addVerificationSystemMessage(peerID, context.getString(R.string.verify_mutual_system_message, name))
sendVerificationNotification(context.getString(R.string.verify_mutual_match_title), body, peerID)
}
}
}
meshService.sendVerifyResponse(peerID, parsed.first, parsed.second)
}
}
fun didReceiveVerifyResponse(peerID: String, payload: ByteArray) {
scope.launch {
val resp = VerificationService.parseVerifyResponse(payload) ?: return@launch
val pending = pendingQRVerifications[peerID] ?: return@launch
if (!resp.noiseKeyHex.equals(pending.noiseKeyHex, ignoreCase = true)) return@launch
if (!resp.nonceA.contentEquals(pending.nonceA)) return@launch
val ok = VerificationService.verifyResponseSignature(
noiseKeyHex = resp.noiseKeyHex,
nonceA = resp.nonceA,
signature = resp.signature,
signerPublicKeyHex = pending.signKeyHex
)
if (!ok) return@launch
pendingQRVerifications.remove(peerID)
val fp = meshService.getPeerFingerprint(peerID) ?: return@launch
identityManager.setVerifiedFingerprint(fp, true)
val current = _verifiedFingerprints.value.toMutableSet()
current.add(fp)
_verifiedFingerprints.value = current
val name = resolvePeerDisplayName(peerID)
identityManager.cacheFingerprintNickname(fp, name)
val noiseKeyHex = try {
meshService.getPeerInfo(peerID)?.noisePublicKey?.hexEncodedString()
} catch (_: Exception) {
null
}
if (noiseKeyHex != null) {
identityManager.cachePeerNoiseKey(peerID, noiseKeyHex)
identityManager.cacheNoiseFingerprint(noiseKeyHex, fp)
}
addVerificationSystemMessage(peerID, context.getString(R.string.verify_success_system_message, name))
sendVerificationNotification(context.getString(R.string.verify_success_title), context.getString(R.string.verify_success_body, name), peerID)
val lastChallenge = lastInboundVerifyChallengeAt[fp] ?: 0L
if (System.currentTimeMillis() - lastChallenge < 600_000L) {
val lastToast = lastMutualToastAt[fp] ?: 0L
if (System.currentTimeMillis() - lastToast > 60_000L) {
lastMutualToastAt[fp] = System.currentTimeMillis()
val body = context.getString(R.string.verify_mutual_match_body, name)
addVerificationSystemMessage(peerID, context.getString(R.string.verify_mutual_system_message, name))
sendVerificationNotification(context.getString(R.string.verify_mutual_match_title), body, peerID)
}
}
}
}
fun getPeerFingerprintForDisplay(peerID: String): String? {
val fromMap = state.getPeerFingerprintsValue()[peerID]
if (fromMap != null) return fromMap
val hexRegex = Regex("^[0-9a-fA-F]+$")
return try {
when {
peerID.length == 64 && peerID.matches(hexRegex) -> {
identityManager.getCachedNoiseFingerprint(peerID)?.let { return it }
fingerprintFromNoiseHex(peerID)?.also { identityManager.cacheNoiseFingerprint(peerID, it) }
}
peerID.length == 16 && peerID.matches(hexRegex) -> {
val meshFp = meshService.getPeerFingerprint(peerID)
if (meshFp != null) return meshFp
identityManager.getCachedPeerFingerprint(peerID)?.let { return it }
identityManager.getCachedNoiseKey(peerID)?.let { noiseHex ->
identityManager.getCachedNoiseFingerprint(noiseHex)?.let { return it }
return fingerprintFromNoiseHex(noiseHex)?.also { identityManager.cacheNoiseFingerprint(noiseHex, it) }
}
val favorite = try {
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
} catch (_: Exception) {
null
}
favorite?.peerNoisePublicKey?.let { fingerprintFromNoiseBytes(it) }
}
peerID.startsWith("nostr_") -> {
val pubHex = GeohashAliasRegistry.get(peerID)
val noiseKey = pubHex?.let {
FavoritesPersistenceService.shared.findNoiseKey(it)
}
noiseKey?.let {
val noiseHex = it.hexEncodedString()
identityManager.getCachedNoiseFingerprint(noiseHex) ?: fingerprintFromNoiseBytes(it)
}
}
peerID.startsWith("nostr:") -> {
val prefix = peerID.removePrefix("nostr:").lowercase()
val pubHex = GeohashAliasRegistry
.snapshot()
.values
.firstOrNull { it.lowercase().startsWith(prefix) }
val noiseKey = pubHex?.let {
FavoritesPersistenceService.shared.findNoiseKey(it)
}
noiseKey?.let {
val noiseHex = it.hexEncodedString()
identityManager.getCachedNoiseFingerprint(noiseHex) ?: fingerprintFromNoiseBytes(it)
}
}
else -> {
val meshFp = meshService.getPeerFingerprint(peerID)
if (meshFp != null) return meshFp
identityManager.getCachedPeerFingerprint(peerID)?.let { return it }
identityManager.getCachedNoiseKey(peerID)?.let { noiseHex ->
identityManager.getCachedNoiseFingerprint(noiseHex)?.let { return it }
return fingerprintFromNoiseHex(noiseHex)?.also { identityManager.cacheNoiseFingerprint(noiseHex, it) }
}
val favorite = try {
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
} catch (_: Exception) {
null
}
favorite?.peerNoisePublicKey?.let { fingerprintFromNoiseBytes(it) }
}
}
} catch (_: Exception) {
null
}
}
fun resolvePeerDisplayNameForFingerprint(peerID: String): String {
val nicknameMap = state.peerNicknames.value
nicknameMap[peerID]?.let { return it }
try {
meshService.getPeerInfo(peerID)?.nickname?.let { return it }
} catch (_: Exception) { }
val fingerprint = getPeerFingerprintForDisplay(peerID)
fingerprint?.let { fp ->
identityManager.getCachedFingerprintNickname(fp)?.let { cached ->
if (cached.isNotBlank()) return cached
}
}
val hexRegex = Regex("^[0-9a-fA-F]+$")
if (peerID.length == 64 && peerID.matches(hexRegex)) {
val noiseKeyBytes = try {
peerID.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
} catch (_: Exception) { null }
val favorite = noiseKeyBytes?.let {
FavoritesPersistenceService.shared.getFavoriteStatus(it)
}
favorite?.peerNickname?.takeIf { it.isNotBlank() }?.let { return it }
}
if (peerID.length == 16 && peerID.matches(hexRegex)) {
val favorite = try {
FavoritesPersistenceService.shared.getFavoriteStatus(peerID)
} catch (_: Exception) {
null
}
favorite?.peerNickname?.takeIf { it.isNotBlank() }?.let { return it }
}
return peerID.take(8)
}
fun getMyFingerprint(): String {
return meshService.getIdentityFingerprint()
}
fun verifyFingerprintValue(fingerprint: String) {
if (fingerprint.isBlank()) return
identityManager.setVerifiedFingerprint(fingerprint, true)
val current = _verifiedFingerprints.value.toMutableSet()
current.add(fingerprint)
_verifiedFingerprints.value = current
}
fun unverifyFingerprintValue(fingerprint: String) {
if (fingerprint.isBlank()) return
identityManager.setVerifiedFingerprint(fingerprint, false)
val current = _verifiedFingerprints.value.toMutableSet()
current.remove(fingerprint)
_verifiedFingerprints.value = current
}
private fun addVerificationSystemMessage(peerID: String, text: String) {
val msg = BitchatMessage(
sender = "system",
content = text,
timestamp = Date(),
isRelay = false,
isPrivate = true,
senderPeerID = peerID
)
messageManager.addPrivateMessageNoUnread(peerID, msg)
}
private fun resolvePeerDisplayName(peerID: String): String {
val nick = try { meshService.getPeerInfo(peerID)?.nickname } catch (_: Exception) { null }
return nick ?: peerID.take(8)
}
private fun sendVerificationNotification(title: String, body: String, peerID: String) {
notificationManager.showVerificationNotification(title, body, peerID)
}
private fun fingerprintFromNoiseHex(noiseHex: String): String? {
val bytes = noiseHex.dataFromHexString() ?: return null
return fingerprintFromNoiseBytes(bytes)
}
fun fingerprintFromNoiseBytes(bytes: ByteArray): String {
val hash = MessageDigest.getInstance("SHA-256").digest(bytes)
return hash.hexEncodedString()
}
private data class PendingVerification(
val noiseKeyHex: String,
val signKeyHex: String,
val nonceA: ByteArray,
val startedAtMs: Long,
val sent: Boolean
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PendingVerification
if (startedAtMs != other.startedAtMs) return false
if (sent != other.sent) return false
if (noiseKeyHex != other.noiseKeyHex) return false
if (signKeyHex != other.signKeyHex) return false
if (!nonceA.contentEquals(other.nonceA)) return false
return true
}
override fun hashCode(): Int {
var result = startedAtMs.hashCode()
result = 31 * result + sent.hashCode()
result = 31 * result + noiseKeyHex.hashCode()
result = 31 * result + signKeyHex.hashCode()
result = 31 * result + nonceA.contentHashCode()
return result
}
}
}
@@ -0,0 +1,496 @@
package com.bitchat.android.ui
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.camera.compose.CameraXViewfinder
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview
import androidx.camera.core.SurfaceRequest
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.viewfinder.core.ImplementationMode
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.QrCode
import androidx.compose.material.icons.outlined.QrCodeScanner
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.graphics.createBitmap
import androidx.core.graphics.set
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bitchat.android.R
import com.bitchat.android.services.VerificationService
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import kotlinx.coroutines.flow.MutableStateFlow
import com.google.zxing.BarcodeFormat
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.QRCodeWriter
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VerificationSheet(
isPresented: Boolean,
onDismiss: () -> Unit,
viewModel: ChatViewModel,
modifier: Modifier = Modifier
) {
if (!isPresented) return
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val isDark = isSystemInDarkTheme()
val accent = if (isDark) Color.Green else Color(0xFF008000)
val boxColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.06f)
var showingScanner by remember { mutableStateOf(false) }
val nickname by viewModel.nickname.collectAsStateWithLifecycle()
val npub = remember {
viewModel.getCurrentNpub()
}
val qrString = remember(nickname, npub) {
viewModel.buildMyQRString(nickname, npub)
}
ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = MaterialTheme.colorScheme.background,
dragHandle = null
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
VerificationHeader(
accent = accent,
onClose = onDismiss
)
Box(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (showingScanner) {
QRScannerPanel(
accent = accent,
boxColor = boxColor,
onScan = { code ->
val qr = VerificationService.verifyScannedQR(code)
if (qr != null && viewModel.beginQRVerification(qr)) {
showingScanner = false
}
}
)
} else {
MyQrPanel(
qrString = qrString,
accent = accent,
boxColor = boxColor,
)
}
}
}
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
ToggleVerificationModeButton(
showingScanner = showingScanner,
onToggle = { showingScanner = !showingScanner }
)
val peerID by viewModel.selectedPrivateChatPeer.collectAsStateWithLifecycle()
val fingerprints by viewModel.verifiedFingerprints.collectAsStateWithLifecycle()
if (peerID != null) {
val fingerprint = viewModel.meshService.getPeerFingerprint(peerID!!)
if (fingerprint != null && fingerprints.contains(fingerprint)) {
Button(
onClick = { viewModel.unverifyFingerprint(peerID!!) },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
contentColor = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.verify_remove),
fontFamily = FontFamily.Monospace,
fontSize = 12.sp
)
}
}
}
}
}
}
}
@Composable
private fun VerificationHeader(
accent: Color,
onClose: () -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.verify_title).uppercase(),
fontSize = 14.sp,
fontFamily = FontFamily.Monospace,
color = accent
)
CloseButton(onClick = onClose)
}
}
@Composable
private fun MyQrPanel(
qrString: String,
accent: Color,
boxColor: Color,
) {
Text(
text = stringResource(R.string.verify_my_qr_title),
fontSize = 16.sp,
fontFamily = FontFamily.Monospace,
color = accent,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
QRCodeCard(qrString = qrString, boxColor = boxColor)
}
@Composable
private fun ToggleVerificationModeButton(
showingScanner: Boolean,
onToggle: () -> Unit
) {
Button(
onClick = onToggle,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
contentColor = MaterialTheme.colorScheme.onSurface
),
modifier = Modifier.fillMaxWidth()
) {
if (showingScanner) {
Icon(
imageVector = Icons.Outlined.QrCode,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.verify_show_my_qr),
fontFamily = FontFamily.Monospace,
fontSize = 13.sp
)
} else {
Icon(
imageVector = Icons.Outlined.QrCodeScanner,
contentDescription = null,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = stringResource(R.string.verify_scan_someone),
fontFamily = FontFamily.Monospace,
fontSize = 13.sp
)
}
}
}
@Composable
private fun QRCodeCard(qrString: String, boxColor: Color) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(boxColor, RoundedCornerShape(12.dp))
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (qrString.isNotBlank()) {
QRCodeImage(data = qrString, size = 220.dp)
} else {
Box(
modifier = Modifier
.size(220.dp)
.background(Color.Transparent, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(R.string.verify_qr_unavailable),
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
SelectionContainer {
Text(
text = qrString,
fontFamily = FontFamily.Monospace,
fontSize = 11.sp,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
@Composable
private fun QRCodeImage(data: String, size: Dp) {
val sizePx = with(LocalDensity.current) { size.toPx().toInt() }
val bitmap = remember(data, sizePx) { generateQrBitmap(data, sizePx) }
if (bitmap != null) {
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = null,
modifier = Modifier.size(size)
)
}
}
private fun generateQrBitmap(data: String, sizePx: Int): Bitmap? {
if (data.isBlank() || sizePx <= 0) return null
return try {
val matrix = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, sizePx, sizePx)
bitmapFromMatrix(matrix)
} catch (_: Exception) {
null
}
}
private fun bitmapFromMatrix(matrix: BitMatrix): Bitmap {
val width = matrix.width
val height = matrix.height
val bitmap = createBitmap(width, height)
for (x in 0 until width) {
for (y in 0 until height) {
bitmap[x, y] =
if (matrix[x, y]) android.graphics.Color.BLACK else android.graphics.Color.WHITE
}
}
return bitmap
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun QRScannerPanel(
onScan: (String) -> Unit,
accent: Color,
boxColor: Color,
modifier: Modifier = Modifier
) {
val permissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var lastValid by remember { mutableStateOf<String?>(null) }
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val cameraExecutor: ExecutorService = remember { Executors.newSingleThreadExecutor() }
val surfaceRequests = remember { MutableStateFlow<SurfaceRequest?>(null) }
val surfaceRequest by surfaceRequests.collectAsState(initial = null)
val mainHandler = remember { Handler(Looper.getMainLooper()) }
val onCodeState = rememberUpdatedState(onScan)
val analyzer = remember {
QRCodeAnalyzer { text ->
mainHandler.post {
if (text == lastValid) return@post
lastValid = text
onCodeState.value(text)
}
}
}
DisposableEffect(permissionState.status.isGranted) {
var cameraProvider: ProcessCameraProvider? = null
if (permissionState.status.isGranted) {
val executor = ContextCompat.getMainExecutor(context)
cameraProviderFuture.addListener(
{
val provider = cameraProviderFuture.get()
cameraProvider = provider
val preview = Preview.Builder()
.build()
.also { it.setSurfaceProvider { request -> surfaceRequests.value = request } }
val analysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also { it.setAnalyzer(cameraExecutor, analyzer) }
runCatching {
provider.unbindAll()
provider.bindToLifecycle(
lifecycleOwner,
CameraSelector.DEFAULT_BACK_CAMERA,
preview,
analysis
)
}.onFailure {
Log.w("VerificationSheet", "Failed to bind camera: ${it.message}")
}
},
executor
)
}
onDispose {
surfaceRequests.value = null
runCatching { cameraProvider?.unbindAll() }
}
}
DisposableEffect(Unit) {
onDispose { cameraExecutor.shutdown() }
}
Column(
modifier = modifier
.fillMaxWidth()
.background(boxColor, RoundedCornerShape(12.dp))
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.verify_scan_prompt_friend),
fontSize = 16.sp,
fontFamily = FontFamily.Monospace,
color = accent,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center
)
if (permissionState.status.isGranted) {
surfaceRequest?.let { request ->
CameraXViewfinder(
surfaceRequest = request,
implementationMode = ImplementationMode.EMBEDDED,
modifier = Modifier
.size(220.dp)
.clipToBounds()
)
}
} else {
Text(
text = stringResource(R.string.verify_camera_permission),
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
)
Button(
onClick = { permissionState.launchPermissionRequest() },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
contentColor = MaterialTheme.colorScheme.onSurface
)
) {
Text(
text = stringResource(R.string.verify_request_camera),
fontFamily = FontFamily.Monospace,
fontSize = 12.sp
)
}
}
}
}
private class QRCodeAnalyzer(
private val onCode: (String) -> Unit
) : ImageAnalysis.Analyzer {
private val scanner = BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
)
@ExperimentalGetImage
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image ?: run {
imageProxy.close()
return
}
val input = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
scanner.process(input)
.addOnSuccessListener { barcodes ->
val text = barcodes.firstOrNull()?.rawValue
if (!text.isNullOrBlank()) onCode(text)
}
.addOnCompleteListener { imageProxy.close() }
}
}
@@ -58,6 +58,10 @@ object AppConstants {
const val HIGH_NONCE_WARNING_THRESHOLD: Long = 1_000_000_000L
}
object Verification {
const val QR_MAX_AGE_SECONDS: Long = 300L // 5 minutes
}
object Protocol {
const val COMPRESSION_THRESHOLD_BYTES: Int = 100
}
+36
View File
@@ -363,4 +363,40 @@
<string name="dismiss">إغلاق</string>
<string name="location_notes_input_placeholder">أضف ملاحظة لهذا المكان</string>
<string name="mesh_service_notification_content">الشبكة تعمل — %1$d أقران</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -350,4 +350,40 @@
</plurals>
<string name="mesh_service_notification_content">মেশ চলছে — %1$d পিয়ার</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Aus Favoriten entfernen</string>
<string name="cd_add_bookmark">Lesezeichen hinzufügen</string>
<!-- ChatKopf & Barrierefreiheit -->
<!-- ChatKopf &amp; Barrierefreiheit -->
<string name="chat_back">zurück</string>
<string name="chat_channel_prefix">Kanal: %1$s</string>
<string name="chat_leave">verlassen</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">OnlineGeohashKanäle</string>
<string name="cd_end_to_end_encryption">EndezuEndeVerschlüsselung</string>
<!-- Bilder & Dateien -->
<!-- Bilder &amp; Dateien -->
<string name="image_page_of">Bild %1$d von %2$d</string>
<string name="image_unavailable">Bild nicht verfügbar</string>
<string name="image_saved_to_downloads">Bild in "Downloads" gespeichert</string>
@@ -364,4 +364,40 @@
<string name="dismiss">schließen</string>
<string name="location_notes_input_placeholder">füge eine Notiz zu diesem Ort hinzu</string>
<string name="mesh_service_notification_content">Mesh läuft — %1$d Peers</string>
<string name="verify_title">verifizieren</string>
<string name="verify_my_qr_title">scannen zur verifizierung</string>
<string name="verify_scan_prompt_friend">anderen qr scannen</string>
<string name="verify_scan_someone">anderen qr scannen</string>
<string name="verify_show_my_qr">mein qr zeigen</string>
<string name="verify_remove">verifizierung entfernen</string>
<string name="verify_qr_unavailable">qr nicht verfügbar</string>
<string name="verify_camera_permission">kamera-berechtigung nötig</string>
<string name="verify_request_camera">kamera aktivieren</string>
<string name="verify_paste_label">verifizierungs-url einfügen</string>
<string name="verify_validate">validieren</string>
<string name="verify_scanned">verifizierung angefordert</string>
<string name="security_verification_title">sicherheitsüberprüfung</string>
<string name="fingerprint_their">ihr fingerabdruck</string>
<string name="fingerprint_yours">dein fingerabdruck</string>
<string name="fingerprint_pending">handshake ausstehend</string>
<string name="fingerprint_no_peer">privatchat öffnen</string>
<string name="fingerprint_status_verified">verschlüsselt &amp; verifiziert</string>
<string name="fingerprint_status_encrypted">verschlüsselt</string>
<string name="fingerprint_status_handshaking">handshake</string>
<string name="fingerprint_status_failed">handshake fehlgeschlagen</string>
<string name="fingerprint_status_uninitialized">unverschlüsselt</string>
<string name="fingerprint_verified_label">verifiziert</string>
<string name="fingerprint_verified_message">du hast diese person verifiziert.</string>
<string name="fingerprint_not_verified_label">nicht verifiziert</string>
<string name="fingerprint_not_verified_message_fmt">vergleiche fingerabdrücke mit %1$s.</string>
<string name="fingerprint_mark_verified">als verifiziert markieren</string>
<string name="fingerprint_start_handshake">handshake starten</string>
<string name="fingerprint_copy">kopieren</string>
<string name="verify_mutual_match_title">Gegenseitige Verifizierung</string>
<string name="verify_mutual_match_body">Du und %1$s habt euch verifiziert</string>
<string name="verify_mutual_system_message">gegenseitige verifizierung mit %1$s</string>
<string name="verify_success_title">Verifiziert</string>
<string name="verify_success_body">Du hast %1$s verifiziert</string>
<string name="verify_success_system_message">verifiziert %1$s</string>
</resources>
+36
View File
@@ -363,4 +363,40 @@
<string name="dismiss">cerrar</string>
<string name="location_notes_input_placeholder">agrega una nota para este lugar</string>
<string name="mesh_service_notification_content">Mesh en ejecución — %1$d pares</string>
<string name="verify_title">verificar</string>
<string name="verify_my_qr_title">escanear para verificarme</string>
<string name="verify_scan_prompt_friend">escanear qr de otro</string>
<string name="verify_scan_someone">escanear qr de otro</string>
<string name="verify_show_my_qr">mostrar mi qr</string>
<string name="verify_remove">eliminar verificación</string>
<string name="verify_qr_unavailable">qr no disponible</string>
<string name="verify_camera_permission">se necesita permiso de cámara</string>
<string name="verify_request_camera">activar cámara</string>
<string name="verify_paste_label">pegar url de verificación</string>
<string name="verify_validate">validar</string>
<string name="verify_scanned">verificación solicitada</string>
<string name="security_verification_title">verificación de seguridad</string>
<string name="fingerprint_their">su huella digital</string>
<string name="fingerprint_yours">tu huella digital</string>
<string name="fingerprint_pending">intercambio pendiente</string>
<string name="fingerprint_no_peer">abrir chat privado para ver</string>
<string name="fingerprint_status_verified">cifrado y verificado</string>
<string name="fingerprint_status_encrypted">cifrado</string>
<string name="fingerprint_status_handshaking">negociando</string>
<string name="fingerprint_status_failed">falló negociación</string>
<string name="fingerprint_status_uninitialized">no cifrado</string>
<string name="fingerprint_verified_label">verificado</string>
<string name="fingerprint_verified_message">has verificado esta identidad.</string>
<string name="fingerprint_not_verified_label">no verificado</string>
<string name="fingerprint_not_verified_message_fmt">compara huellas con %1$s por canal seguro.</string>
<string name="fingerprint_mark_verified">marcar verificado</string>
<string name="fingerprint_start_handshake">iniciar handshake</string>
<string name="fingerprint_copy">copiar</string>
<string name="verify_mutual_match_title">Verificación mutua</string>
<string name="verify_mutual_match_body">Tú y %1$s se verificaron</string>
<string name="verify_mutual_system_message">verificación mutua con %1$s</string>
<string name="verify_success_title">Verificado</string>
<string name="verify_success_body">Verificaste a %1$s</string>
<string name="verify_success_system_message">verificado %1$s</string>
</resources>
+36
View File
@@ -350,4 +350,40 @@
</plurals>
<string name="mesh_service_notification_content">مش در حال اجرا — %1$d همتا</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -363,4 +363,40 @@
</plurals>
<string name="mesh_service_notification_content">Tumatakbo ang Mesh — %1$d na peer</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Retirer des favoris</string>
<string name="cd_add_bookmark">Ajouter un signet</string>
<!-- Entête de chat & accessibilité -->
<!-- Entête de chat &amp; accessibilité -->
<string name="chat_back">retour</string>
<string name="chat_channel_prefix">canal : %1$s</string>
<string name="chat_leave">quitter</string>
@@ -165,7 +165,7 @@
<string name="cd_online_geohash_channels">Canaux Geohash en ligne</string>
<string name="cd_end_to_end_encryption">Chiffrement de bout en bout</string>
<!-- Images & fichiers -->
<!-- Images &amp; fichiers -->
<string name="image_page_of">Image %1$d sur %2$d</string>
<string name="image_unavailable">Image indisponible</string>
<string name="image_saved_to_downloads">Image enregistrée dans "Téléchargements"</string>
@@ -377,4 +377,40 @@
<string name="dismiss">fermer</string>
<string name="location_notes_input_placeholder">ajoutez une note pour cet endroit</string>
<string name="mesh_service_notification_content">Mesh actif — %1$d pairs</string>
<string name="verify_title">vérifier</string>
<string name="verify_my_qr_title">scanner pour me vérifier</string>
<string name="verify_scan_prompt_friend">scanner un autre qr</string>
<string name="verify_scan_someone">scanner un autre qr</string>
<string name="verify_show_my_qr">afficher mon qr</string>
<string name="verify_remove">supprimer la vérification</string>
<string name="verify_qr_unavailable">qr indisponible</string>
<string name="verify_camera_permission">permission caméra requise</string>
<string name="verify_request_camera">activer caméra</string>
<string name="verify_paste_label">coller url de vérification</string>
<string name="verify_validate">valider</string>
<string name="verify_scanned">vérification demandée</string>
<string name="security_verification_title">vérification de sécurité</string>
<string name="fingerprint_their">leur empreinte</string>
<string name="fingerprint_yours">votre empreinte</string>
<string name="fingerprint_pending">négociation en cours</string>
<string name="fingerprint_no_peer">ouvrir un chat privé</string>
<string name="fingerprint_status_verified">chiffré &amp; vérifié</string>
<string name="fingerprint_status_encrypted">chiffré</string>
<string name="fingerprint_status_handshaking">négociation</string>
<string name="fingerprint_status_failed">échec négociation</string>
<string name="fingerprint_status_uninitialized">non chiffré</string>
<string name="fingerprint_verified_label">vérifié</string>
<string name="fingerprint_verified_message">vous avez vérifié cette personne.</string>
<string name="fingerprint_not_verified_label">non vérifié</string>
<string name="fingerprint_not_verified_message_fmt">comparez avec %1$s via canal sûr.</string>
<string name="fingerprint_mark_verified">marquer vérifié</string>
<string name="fingerprint_start_handshake">lancer handshake</string>
<string name="fingerprint_copy">copier</string>
<string name="verify_mutual_match_title">Vérification mutuelle</string>
<string name="verify_mutual_match_body">Vous et %1$s êtes vérifiés</string>
<string name="verify_mutual_system_message">vérification mutuelle avec %1$s</string>
<string name="verify_success_title">Vérifié</string>
<string name="verify_success_body">Vous avez vérifié %1$s</string>
<string name="verify_success_system_message">vérifié %1$s</string>
</resources>
+36
View File
@@ -15,5 +15,41 @@
<string name="dismiss">סגור</string>
<string name="location_notes_input_placeholder">הוסף הערה למקום זה</string>
<string name="mesh_service_notification_content">רשת Mesh פועלת — %1$d עמיתים</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+37 -1
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">पसंदीदा से हटाएँ</string>
<string name="cd_add_bookmark">बुकमार्क जोड़ें</string>
<!-- चैट हेडर & एक्सेसिबिलिटी -->
<!-- चैट हेडर &amp; एक्सेसिबिलिटी -->
<string name="chat_back">वापस</string>
<string name="chat_channel_prefix">चैनल: %1$s</string>
<string name="chat_leave">छोड़ें</string>
@@ -363,4 +363,40 @@
<string name="dismiss">बंद करें</string>
<string name="location_notes_input_placeholder">इस स्थान के लिए एक नोट जोड़ें</string>
<string name="mesh_service_notification_content">मेश चल रहा है — %1$d पीयर्स</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Hapus dari favorit</string>
<string name="cd_add_bookmark">Tambah bookmark</string>
<!-- Header chat & aksesibilitas -->
<!-- Header chat &amp; aksesibilitas -->
<string name="chat_back">kembali</string>
<string name="chat_channel_prefix">Channel: %1$s</string>
<string name="chat_leave">keluar</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">Channel Geohash Online</string>
<string name="cd_end_to_end_encryption">Enkripsi end-to-end</string>
<!-- Gambar & file -->
<!-- Gambar &amp; file -->
<string name="image_page_of">Gambar %1$d dari %2$d</string>
<string name="image_unavailable">Gambar tidak tersedia</string>
<string name="image_saved_to_downloads">Gambar disimpan ke \"Downloads\"</string>
@@ -363,4 +363,40 @@
<string name="dismiss">tutup</string>
<string name="location_notes_input_placeholder">tambahkan catatan untuk tempat ini</string>
<string name="mesh_service_notification_content">Mesh berjalan — %1$d peer</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -397,4 +397,40 @@
<string name="dismiss">chiudi</string>
<string name="location_notes_input_placeholder">aggiungi una nota per questo luogo</string>
<string name="mesh_service_notification_content">Mesh in esecuzione — %1$d peer</string>
<string name="verify_title">verifica</string>
<string name="verify_my_qr_title">scansiona per verificarmi</string>
<string name="verify_scan_prompt_friend">scansiona qr altrui</string>
<string name="verify_scan_someone">scansiona qr altrui</string>
<string name="verify_show_my_qr">mostra mio qr</string>
<string name="verify_remove">rimuovi verifica</string>
<string name="verify_qr_unavailable">qr non disponibile</string>
<string name="verify_camera_permission">permesso fotocamera necessario</string>
<string name="verify_request_camera">abilita fotocamera</string>
<string name="verify_paste_label">incolla url verifica</string>
<string name="verify_validate">convalida</string>
<string name="verify_scanned">verifica richiesta</string>
<string name="security_verification_title">verifica sicurezza</string>
<string name="fingerprint_their">loro impronta</string>
<string name="fingerprint_yours">tua impronta</string>
<string name="fingerprint_pending">handshake in corso</string>
<string name="fingerprint_no_peer">apri chat privata</string>
<string name="fingerprint_status_verified">criptato &amp; verificato</string>
<string name="fingerprint_status_encrypted">criptato</string>
<string name="fingerprint_status_handshaking">handshake</string>
<string name="fingerprint_status_failed">handshake fallito</string>
<string name="fingerprint_status_uninitialized">non criptato</string>
<string name="fingerprint_verified_label">verificato</string>
<string name="fingerprint_verified_message">hai verificato questa persona.</string>
<string name="fingerprint_not_verified_label">non verificato</string>
<string name="fingerprint_not_verified_message_fmt">confronta con %1$s via canale sicuro.</string>
<string name="fingerprint_mark_verified">segna verificato</string>
<string name="fingerprint_start_handshake">avvia handshake</string>
<string name="fingerprint_copy">copia</string>
<string name="verify_mutual_match_title">Verifica reciproca</string>
<string name="verify_mutual_match_body">Tu e %1$s vi siete verificati</string>
<string name="verify_mutual_system_message">verifica reciproca con %1$s</string>
<string name="verify_success_title">Verificato</string>
<string name="verify_success_body">Hai verificato %1$s</string>
<string name="verify_success_system_message">verificato %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">お気に入りから削除</string>
<string name="cd_add_bookmark">ブックマークに追加</string>
<!-- チャットヘッダー & アクセシビリティ -->
<!-- チャットヘッダー &amp; アクセシビリティ -->
<string name="chat_back">戻る</string>
<string name="chat_channel_prefix">チャンネル: %1$s</string>
<string name="chat_leave">退出</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">オンライン Geohash チャンネル</string>
<string name="cd_end_to_end_encryption">エンドツーエンド暗号化</string>
<!-- 画像 & ファイル -->
<!-- 画像 &amp; ファイル -->
<string name="image_page_of">画像 %1$d / %2$d</string>
<string name="image_unavailable">画像は利用できません</string>
<string name="image_saved_to_downloads">画像を「ダウンロード」に保存しました</string>
@@ -363,4 +363,40 @@
<string name="dismiss">閉じる</string>
<string name="location_notes_input_placeholder">この場所へのメモを追加</string>
<string name="mesh_service_notification_content">メッシュ実行中 — %1$d ピア</string>
<string name="verify_title">検証</string>
<string name="verify_my_qr_title">私を検証するためにスキャン</string>
<string name="verify_scan_prompt_friend">他人のQRをスキャン</string>
<string name="verify_scan_someone">他人のQRをスキャン</string>
<string name="verify_show_my_qr">QRコードを表示</string>
<string name="verify_remove">検証を削除</string>
<string name="verify_qr_unavailable">QR利用不可</string>
<string name="verify_camera_permission">QRスキャンにカメラ権限が必要です</string>
<string name="verify_request_camera">カメラを有効化</string>
<string name="verify_paste_label">検証URLを貼り付け</string>
<string name="verify_validate">検証する</string>
<string name="verify_scanned">検証をリクエストしました</string>
<string name="security_verification_title">セキュリティ検証</string>
<string name="fingerprint_their">相手の指紋</string>
<string name="fingerprint_yours">あなたの指紋</string>
<string name="fingerprint_pending">ハンドシェイク保留中</string>
<string name="fingerprint_no_peer">指紋を見るにはプライベートチャットを開いてください</string>
<string name="fingerprint_status_verified">暗号化&検証済み</string>
<string name="fingerprint_status_encrypted">暗号化済み</string>
<string name="fingerprint_status_handshaking">ハンドシェイク中</string>
<string name="fingerprint_status_failed">ハンドシェイク失敗</string>
<string name="fingerprint_status_uninitialized">未暗号化</string>
<string name="fingerprint_verified_label">検証済み</string>
<string name="fingerprint_verified_message">この人物の身元を検証しました。</string>
<string name="fingerprint_not_verified_label">未検証</string>
<string name="fingerprint_not_verified_message_fmt">安全な経路で %1$s と指紋を比較してください。</string>
<string name="fingerprint_mark_verified">検証済みにする</string>
<string name="fingerprint_start_handshake">ハンドシェイク開始</string>
<string name="fingerprint_copy">コピー</string>
<string name="verify_mutual_match_title">相互検証</string>
<string name="verify_mutual_match_body">あなたと %1$s は相互に検証しました</string>
<string name="verify_mutual_system_message">%1$s と相互検証しました</string>
<string name="verify_success_title">検証済み</string>
<string name="verify_success_body">%1$s を検証しました</string>
<string name="verify_success_system_message">%1$s を検証しました</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">რჩეულებიდან წაშლა</string>
<string name="cd_add_bookmark">სანიშნეს დამატება</string>
<!-- ჩათის სათაური & ხელმისაწვდომობა -->
<!-- ჩათის სათაური &amp; ხელმისაწვდომობა -->
<string name="chat_back">უკან</string>
<string name="chat_channel_prefix">არხი: %1$s</string>
<string name="chat_leave">გასვლა</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">ონლაინ geohash არხები</string>
<string name="cd_end_to_end_encryption">ბოლო-მდე დაშიფვრა</string>
<!-- სურათები & ფაილები -->
<!-- სურათები &amp; ფაილები -->
<string name="image_page_of">სურათი %1$d %2$d-დან</string>
<string name="image_unavailable">სურათი მიუწვდომელია</string>
<string name="image_saved_to_downloads">სურათი შენახულია "ჩამოტვირთვებში"</string>
@@ -350,4 +350,40 @@
</plurals>
<string name="mesh_service_notification_content">Mesh გაშვებულია — %1$d პირები</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">즐겨찾기에서 제거</string>
<string name="cd_add_bookmark">북마크 추가</string>
<!-- 채팅 헤더 & 접근성 -->
<!-- 채팅 헤더 &amp; 접근성 -->
<string name="chat_back">뒤로</string>
<string name="chat_channel_prefix">채널: %1$s</string>
<string name="chat_leave">나가기</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">온라인 geohash 채널</string>
<string name="cd_end_to_end_encryption">종단간 암호화</string>
<!-- 이미지 & 파일 -->
<!-- 이미지 &amp; 파일 -->
<string name="image_page_of">이미지 %1$d / %2$d</string>
<string name="image_unavailable">이미지를 사용할 수 없음</string>
<string name="image_saved_to_downloads">이미지가 \"Downloads\"에 저장됨</string>
@@ -363,4 +363,40 @@
<string name="dismiss">닫기</string>
<string name="location_notes_input_placeholder">이 장소에 노트 추가</string>
<string name="mesh_service_notification_content">메시 실행 중 — %1$d 피어</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Hanesorana amin\'ny ankafizina</string>
<string name="cd_add_bookmark">Hanampy bookmark</string>
<!-- Lohan\'ny resaka & fahafaha-miditra -->
<!-- Lohan\'ny resaka &amp; fahafaha-miditra -->
<string name="chat_back">miverina</string>
<string name="chat_channel_prefix">fantsona: %1$s</string>
<string name="chat_leave">hiala</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">Fantsona Geohash an-tserasera</string>
<string name="cd_end_to_end_encryption">Fandokoana Farany amin\'ny Farany</string>
<!-- Sary & rakitra -->
<!-- Sary &amp; rakitra -->
<string name="image_page_of">Sary %1$d amin\'ny %2$d</string>
<string name="image_unavailable">Tsy misy sary</string>
<string name="image_saved_to_downloads">Sary voatahiry ao amin\'ny Downloads</string>
@@ -377,4 +377,40 @@
</plurals>
<string name="mesh_service_notification_content">Mandeha ny Mesh — %1$d peers</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -2,5 +2,41 @@
<resources>
<!-- TODO: Malay translations -->
<string name="mesh_service_notification_content">Mesh sedang berjalan — %1$d rakan</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -363,4 +363,40 @@
</plurals>
<string name="mesh_service_notification_content">मेश चलिरहेको छ — %1$d पियर्स</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+39 -3
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Verwijderen uit favorieten</string>
<string name="cd_add_bookmark">Bladwijzer toevoegen</string>
<!-- Chatkop & toegankelijkheid -->
<!-- Chatkop &amp; toegankelijkheid -->
<string name="chat_back">terug</string>
<string name="chat_channel_prefix">kanaal: %1$s</string>
<string name="chat_leave">verlaten</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">Online geohash-kanalen</string>
<string name="cd_end_to_end_encryption">End-to-end-versleuteling</string>
<!-- Afbeeldingen & bestanden -->
<!-- Afbeeldingen &amp; bestanden -->
<string name="image_page_of">Afbeelding %1$d van %2$d</string>
<string name="image_unavailable">Afbeelding niet beschikbaar</string>
<string name="image_saved_to_downloads">Afbeelding opgeslagen in Downloads</string>
@@ -338,7 +338,7 @@
<string name="join">Deelnemen</string>
<string name="cancel">Annuleren</string>
<!-- Symbolen & speciale strings -->
<!-- Symbolen &amp; speciale strings -->
<string name="at_symbol">@</string>
<string name="app_brand">bitchat/</string>
<string name="channel_count_prefix"> · ⧉ </string>
@@ -395,4 +395,40 @@
<string name="dismiss">sluiten</string>
<string name="location_notes_input_placeholder">voeg een notitie toe voor deze plek</string>
<string name="mesh_service_notification_content">Mesh actief — %1$d peers</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">پسندیدہ توں ہٹاؤ</string>
<string name="cd_add_bookmark">بُک مارک پاؤ</string>
<!-- چیٹ ہیڈر & رسائی پذیری -->
<!-- چیٹ ہیڈر &amp; رسائی پذیری -->
<string name="chat_back">واپس</string>
<string name="chat_channel_prefix">چینل: %1$s</string>
<string name="chat_leave">باہر آؤ</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">آن لائن geohash چینل</string>
<string name="cd_end_to_end_encryption">اینڈ ٹو اینڈ انکرپشن</string>
<!-- تصویراں & فائلز -->
<!-- تصویراں &amp; فائلز -->
<string name="image_page_of">تصویر %1$d وچوں %2$d</string>
<string name="image_unavailable">تصویر دستیاب نئیں</string>
<string name="image_saved_to_downloads">تصویر \"Downloads\" وچ سیو ہو گئی</string>
@@ -350,4 +350,40 @@
</plurals>
<string name="mesh_service_notification_content">میش چل رہا ہے — %1$d ساتھی</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -15,5 +15,41 @@
<string name="dismiss">zamknij</string>
<string name="location_notes_input_placeholder">dodaj notatkę dla tego miejsca</string>
<string name="mesh_service_notification_content">Mesh działa — %1$d peerów</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Remover dos favoritos</string>
<string name="cd_add_bookmark">Adicionar favorito</string>
<!-- Cabeçalho do chat & acessibilidade -->
<!-- Cabeçalho do chat &amp; acessibilidade -->
<string name="chat_back">voltar</string>
<string name="chat_channel_prefix">Canal: %1$s</string>
<string name="chat_leave">sair</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">Canais geohash online</string>
<string name="cd_end_to_end_encryption">Criptografia de ponta a ponta</string>
<!-- Imagens & arquivos -->
<!-- Imagens &amp; arquivos -->
<string name="image_page_of">Imagem %1$d de %2$d</string>
<string name="image_unavailable">Imagem indisponível</string>
<string name="image_saved_to_downloads">Imagem salva em "Downloads"</string>
@@ -363,4 +363,40 @@
<string name="dismiss">fechar</string>
<string name="location_notes_input_placeholder">adicione uma nota para este local</string>
<string name="mesh_service_notification_content">Mesh rodando — %1$d pares</string>
<string name="verify_title">verificar</string>
<string name="verify_my_qr_title">digitalize para verificar-me</string>
<string name="verify_scan_prompt_friend">digitalizar outro qr</string>
<string name="verify_scan_someone">digitalizar outro qr</string>
<string name="verify_show_my_qr">mostrar meu qr</string>
<string name="verify_remove">remover verificação</string>
<string name="verify_qr_unavailable">qr indisponível</string>
<string name="verify_camera_permission">permissão de câmera necessária</string>
<string name="verify_request_camera">ativar câmera</string>
<string name="verify_paste_label">colar url de verificação</string>
<string name="verify_validate">validar</string>
<string name="verify_scanned">verificação solicitada</string>
<string name="security_verification_title">verificação de segurança</string>
<string name="fingerprint_their">impressão digital deles</string>
<string name="fingerprint_yours">sua impressão digital</string>
<string name="fingerprint_pending">handshake pendente</string>
<string name="fingerprint_no_peer">abra chat privado para ver</string>
<string name="fingerprint_status_verified">criptografado e verificado</string>
<string name="fingerprint_status_encrypted">criptografado</string>
<string name="fingerprint_status_handshaking">handshake</string>
<string name="fingerprint_status_failed">falha no handshake</string>
<string name="fingerprint_status_uninitialized">não criptografado</string>
<string name="fingerprint_verified_label">verificado</string>
<string name="fingerprint_verified_message">você verificou esta pessoa.</string>
<string name="fingerprint_not_verified_label">não verificado</string>
<string name="fingerprint_not_verified_message_fmt">compare com %1$s em canal seguro.</string>
<string name="fingerprint_mark_verified">marcar verificado</string>
<string name="fingerprint_start_handshake">iniciar handshake</string>
<string name="fingerprint_copy">copiar</string>
<string name="verify_mutual_match_title">Verificação mútua</string>
<string name="verify_mutual_match_body">Você e %1$s verificaram-se</string>
<string name="verify_mutual_system_message">verificação mútua com %1$s</string>
<string name="verify_success_title">Verificado</string>
<string name="verify_success_body">Você verificou %1$s</string>
<string name="verify_success_system_message">verificou %1$s</string>
</resources>
+36
View File
@@ -363,4 +363,40 @@
<string name="dismiss">fechar</string>
<string name="location_notes_input_placeholder">adicione uma nota para este local</string>
<string name="mesh_service_notification_content">Mesh em execução — %1$d pares</string>
<string name="verify_title">verificar</string>
<string name="verify_my_qr_title">digitalize para verificar-me</string>
<string name="verify_scan_prompt_friend">digitalizar outro qr</string>
<string name="verify_scan_someone">digitalizar outro qr</string>
<string name="verify_show_my_qr">mostrar meu qr</string>
<string name="verify_remove">remover verificação</string>
<string name="verify_qr_unavailable">qr indisponível</string>
<string name="verify_camera_permission">permissão de câmera necessária</string>
<string name="verify_request_camera">ativar câmera</string>
<string name="verify_paste_label">colar url de verificação</string>
<string name="verify_validate">validar</string>
<string name="verify_scanned">verificação solicitada</string>
<string name="security_verification_title">verificação de segurança</string>
<string name="fingerprint_their">impressão digital deles</string>
<string name="fingerprint_yours">sua impressão digital</string>
<string name="fingerprint_pending">handshake pendente</string>
<string name="fingerprint_no_peer">abra chat privado para ver</string>
<string name="fingerprint_status_verified">criptografado e verificado</string>
<string name="fingerprint_status_encrypted">criptografado</string>
<string name="fingerprint_status_handshaking">handshake</string>
<string name="fingerprint_status_failed">falha no handshake</string>
<string name="fingerprint_status_uninitialized">não criptografado</string>
<string name="fingerprint_verified_label">verificado</string>
<string name="fingerprint_verified_message">você verificou esta pessoa.</string>
<string name="fingerprint_not_verified_label">não verificado</string>
<string name="fingerprint_not_verified_message_fmt">compare com %1$s em canal seguro.</string>
<string name="fingerprint_mark_verified">marcar verificado</string>
<string name="fingerprint_start_handshake">iniciar handshake</string>
<string name="fingerprint_copy">copiar</string>
<string name="verify_mutual_match_title">Verificação mútua</string>
<string name="verify_mutual_match_body">Você e %1$s verificaram-se</string>
<string name="verify_mutual_system_message">verificação mútua com %1$s</string>
<string name="verify_success_title">Verificado</string>
<string name="verify_success_body">Você verificou %1$s</string>
<string name="verify_success_system_message">verificou %1$s</string>
</resources>
+36
View File
@@ -353,4 +353,40 @@
<string name="dismiss">закрыть</string>
<string name="location_notes_input_placeholder">добавьте заметку для этого места</string>
<string name="mesh_service_notification_content">Mesh запущен — %1$d пиров</string>
<string name="verify_title">проверить</string>
<string name="verify_my_qr_title">сканируй для проверки</string>
<string name="verify_scan_prompt_friend">сканировать чужой QR</string>
<string name="verify_scan_someone">сканировать чужой QR</string>
<string name="verify_show_my_qr">показать мой QR</string>
<string name="verify_remove">удалить проверку</string>
<string name="verify_qr_unavailable">QR недоступен</string>
<string name="verify_camera_permission">нужен доступ к камере</string>
<string name="verify_request_camera">включить камеру</string>
<string name="verify_paste_label">вставить URL проверки</string>
<string name="verify_validate">проверить</string>
<string name="verify_scanned">запрос проверки</string>
<string name="security_verification_title">проверка безопасности</string>
<string name="fingerprint_their">их отпечаток</string>
<string name="fingerprint_yours">ваш отпечаток</string>
<string name="fingerprint_pending">ожидание рукопожатия</string>
<string name="fingerprint_no_peer">откройте личный чат</string>
<string name="fingerprint_status_verified">шифровано и проверено</string>
<string name="fingerprint_status_encrypted">шифровано</string>
<string name="fingerprint_status_handshaking">рукопожатие</string>
<string name="fingerprint_status_failed">сбой рукопожатия</string>
<string name="fingerprint_status_uninitialized">не шифровано</string>
<string name="fingerprint_verified_label">проверено</string>
<string name="fingerprint_verified_message">вы проверили этого человека.</string>
<string name="fingerprint_not_verified_label">не проверено</string>
<string name="fingerprint_not_verified_message_fmt">сравните с %1$s.</string>
<string name="fingerprint_mark_verified">отметить проверенным</string>
<string name="fingerprint_start_handshake">начать рукопожатие</string>
<string name="fingerprint_copy">копировать</string>
<string name="verify_mutual_match_title">Взаимная проверка</string>
<string name="verify_mutual_match_body">Вы и %1$s проверили друг друга</string>
<string name="verify_mutual_system_message">взаимная проверка с %1$s</string>
<string name="verify_success_title">Проверено</string>
<string name="verify_success_body">Вы проверили %1$s</string>
<string name="verify_success_system_message">проверен %1$s</string>
</resources>
+36
View File
@@ -351,4 +351,40 @@
<string name="dismiss">stäng</string>
<string name="location_notes_input_placeholder">lägg till en anteckning för den här platsen</string>
<string name="mesh_service_notification_content">Mesh körs — %1$d peers</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -2,5 +2,41 @@
<resources>
<!-- TODO: Tamil translations -->
<string name="mesh_service_notification_content">மெஷ் இயங்குகிறது — %1$d பியர்ஸ்</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">เอาออกจากรายการโปรด</string>
<string name="cd_add_bookmark">เพิ่มที่คั่นหน้า</string>
<!-- ส่วนหัวแชท & การช่วยสำหรับการเข้าถึง -->
<!-- ส่วนหัวแชท &amp; การช่วยสำหรับการเข้าถึง -->
<string name="chat_back">กลับ</string>
<string name="chat_channel_prefix">ช่อง: %1$s</string>
<string name="chat_leave">ออก</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">ช่อง geohash ออนไลน์</string>
<string name="cd_end_to_end_encryption">การเข้ารหัสแบบปลายทางถึงปลายทาง</string>
<!-- รูปภาพ & ไฟล์ -->
<!-- รูปภาพ &amp; ไฟล์ -->
<string name="image_page_of">รูปภาพ %1$d จาก %2$d</string>
<string name="image_unavailable">ไม่สามารถใช้รูปภาพได้</string>
<string name="image_saved_to_downloads">บันทึกรูปภาพไว้ใน "ดาวน์โหลด" แล้ว</string>
@@ -350,4 +350,40 @@
</plurals>
<string name="mesh_service_notification_content">Mesh กำลังทำงาน — %1$d เพื่อน</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -351,4 +351,40 @@
<string name="dismiss">kapat</string>
<string name="location_notes_input_placeholder">bu yer için bir not ekleyin</string>
<string name="mesh_service_notification_content">Mesh çalışıyor — %1$d eş</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -2,5 +2,41 @@
<resources>
<!-- TODO: Ukrainian translations -->
<string name="mesh_service_notification_content">Mesh працює — %1$d пірів</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+36
View File
@@ -363,4 +363,40 @@
<string name="dismiss">بند کریں</string>
<string name="location_notes_input_placeholder">اس جگہ کے لیے ایک نوٹ شامل کریں</string>
<string name="mesh_service_notification_content">میش چل رہا ہے — %1$d ساتھی</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
+38 -2
View File
@@ -70,7 +70,7 @@
<string name="cd_remove_favorite">Xóa khỏi yêu thích</string>
<string name="cd_add_bookmark">Thêm bookmark</string>
<!-- Header trò chuyện & khả năng truy cập -->
<!-- Header trò chuyện &amp; khả năng truy cập -->
<string name="chat_back">quay lại</string>
<string name="chat_channel_prefix">Kênh: %1$s</string>
<string name="chat_leave">rời khỏi</string>
@@ -170,7 +170,7 @@
<string name="cd_online_geohash_channels">Kênh Geohash trực tuyến</string>
<string name="cd_end_to_end_encryption">Mã hóa đầu cuối</string>
<!-- Hình ảnh & file -->
<!-- Hình ảnh &amp; file -->
<string name="image_page_of">Hình ảnh %1$d trong %2$d</string>
<string name="image_unavailable">Hình ảnh không có sẵn</string>
<string name="image_saved_to_downloads">Hình ảnh đã lưu vào \"Downloads\"</string>
@@ -350,4 +350,40 @@
</plurals>
<string name="mesh_service_notification_content">Mesh đang chạy — %1$d ngang hàng</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
</resources>
@@ -15,5 +15,41 @@
<string name="dismiss">关闭</string>
<string name="location_notes_input_placeholder">为此地点添加一条笔记</string>
<string name="mesh_service_notification_content">Mesh 运行中 — %1$d 个节点</string>
<string name="verify_title">验证</string>
<string name="verify_my_qr_title">扫描以验证我</string>
<string name="verify_scan_prompt_friend">扫描他人的QR码</string>
<string name="verify_scan_someone">扫描他人的QR码</string>
<string name="verify_show_my_qr">显示我的QR码</string>
<string name="verify_remove">移除验证</string>
<string name="verify_qr_unavailable">QR码不可用</string>
<string name="verify_camera_permission">需要相机权限扫描QR码</string>
<string name="verify_request_camera">启用相机</string>
<string name="verify_paste_label">粘贴验证链接</string>
<string name="verify_validate">验证</string>
<string name="verify_scanned">已请求验证</string>
<string name="security_verification_title">安全验证</string>
<string name="fingerprint_their">对方指纹</string>
<string name="fingerprint_yours">你的指纹</string>
<string name="fingerprint_pending">握手中</string>
<string name="fingerprint_no_peer">打开私聊查看指纹</string>
<string name="fingerprint_status_verified">已加密且已验证</string>
<string name="fingerprint_status_encrypted">已加密</string>
<string name="fingerprint_status_handshaking">握手中</string>
<string name="fingerprint_status_failed">握手失败</string>
<string name="fingerprint_status_uninitialized">未加密</string>
<string name="fingerprint_verified_label">已验证</string>
<string name="fingerprint_verified_message">你已验证此人身份。</string>
<string name="fingerprint_not_verified_label">未验证</string>
<string name="fingerprint_not_verified_message_fmt">请通过安全渠道与 %1$s 比对指纹。</string>
<string name="fingerprint_mark_verified">标记为已验证</string>
<string name="fingerprint_start_handshake">开始握手</string>
<string name="fingerprint_copy">复制</string>
<string name="verify_mutual_match_title">相互验证</string>
<string name="verify_mutual_match_body">你和 %1$s 已相互验证</string>
<string name="verify_mutual_system_message">与 %1$s 相互验证</string>
<string name="verify_success_title">已验证</string>
<string name="verify_success_body">你已验证 %1$s</string>
<string name="verify_success_system_message">已验证 %1$s</string>
</resources>
@@ -15,5 +15,41 @@
<string name="dismiss">關閉</string>
<string name="location_notes_input_placeholder">為此地點新增一則筆記</string>
<string name="mesh_service_notification_content">Mesh 運行中 — %1$d 個節點</string>
<string name="verify_title">验证</string>
<string name="verify_my_qr_title">扫描以验证我</string>
<string name="verify_scan_prompt_friend">扫描他人的QR码</string>
<string name="verify_scan_someone">扫描他人的QR码</string>
<string name="verify_show_my_qr">显示我的QR码</string>
<string name="verify_remove">移除验证</string>
<string name="verify_qr_unavailable">QR码不可用</string>
<string name="verify_camera_permission">需要相机权限扫描QR码</string>
<string name="verify_request_camera">启用相机</string>
<string name="verify_paste_label">粘贴验证链接</string>
<string name="verify_validate">验证</string>
<string name="verify_scanned">已请求验证</string>
<string name="security_verification_title">安全验证</string>
<string name="fingerprint_their">对方指纹</string>
<string name="fingerprint_yours">你的指纹</string>
<string name="fingerprint_pending">握手中</string>
<string name="fingerprint_no_peer">打开私聊查看指纹</string>
<string name="fingerprint_status_verified">已加密且已验证</string>
<string name="fingerprint_status_encrypted">已加密</string>
<string name="fingerprint_status_handshaking">握手中</string>
<string name="fingerprint_status_failed">握手失败</string>
<string name="fingerprint_status_uninitialized">未加密</string>
<string name="fingerprint_verified_label">已验证</string>
<string name="fingerprint_verified_message">你已验证此人身份。</string>
<string name="fingerprint_not_verified_label">未验证</string>
<string name="fingerprint_not_verified_message_fmt">请通过安全渠道与 %1$s 比对指纹。</string>
<string name="fingerprint_mark_verified">标记为已验证</string>
<string name="fingerprint_start_handshake">开始握手</string>
<string name="fingerprint_copy">复制</string>
<string name="verify_mutual_match_title">相互验证</string>
<string name="verify_mutual_match_body">你和 %1$s 已相互验证</string>
<string name="verify_mutual_system_message">与 %1$s 相互验证</string>
<string name="verify_success_title">已验证</string>
<string name="verify_success_body">你已验证 %1$s</string>
<string name="verify_success_system_message">已验证 %1$s</string>
</resources>
+36
View File
@@ -376,4 +376,40 @@
<string name="dismiss">关闭</string>
<string name="location_notes_input_placeholder">为此地点添加一条笔记</string>
<string name="mesh_service_notification_content">Mesh 运行中 — %1$d 个节点</string>
<string name="verify_title">验证</string>
<string name="verify_my_qr_title">扫描以验证我</string>
<string name="verify_scan_prompt_friend">扫描他人的QR码</string>
<string name="verify_scan_someone">扫描他人的QR码</string>
<string name="verify_show_my_qr">显示我的QR码</string>
<string name="verify_remove">移除验证</string>
<string name="verify_qr_unavailable">QR码不可用</string>
<string name="verify_camera_permission">需要相机权限扫描QR码</string>
<string name="verify_request_camera">启用相机</string>
<string name="verify_paste_label">粘贴验证链接</string>
<string name="verify_validate">验证</string>
<string name="verify_scanned">已请求验证</string>
<string name="security_verification_title">安全验证</string>
<string name="fingerprint_their">对方指纹</string>
<string name="fingerprint_yours">你的指纹</string>
<string name="fingerprint_pending">握手中</string>
<string name="fingerprint_no_peer">打开私聊查看指纹</string>
<string name="fingerprint_status_verified">已加密且已验证</string>
<string name="fingerprint_status_encrypted">已加密</string>
<string name="fingerprint_status_handshaking">握手中</string>
<string name="fingerprint_status_failed">握手失败</string>
<string name="fingerprint_status_uninitialized">未加密</string>
<string name="fingerprint_verified_label">已验证</string>
<string name="fingerprint_verified_message">你已验证此人身份。</string>
<string name="fingerprint_not_verified_label">未验证</string>
<string name="fingerprint_not_verified_message_fmt">请通过安全渠道与 %1$s 比对指纹。</string>
<string name="fingerprint_mark_verified">标记为已验证</string>
<string name="fingerprint_start_handshake">开始握手</string>
<string name="fingerprint_copy">复制</string>
<string name="verify_mutual_match_title">相互验证</string>
<string name="verify_mutual_match_body">你和 %1$s 已相互验证</string>
<string name="verify_mutual_system_message">与 %1$s 相互验证</string>
<string name="verify_success_title">已验证</string>
<string name="verify_success_body">你已验证 %1$s</string>
<string name="verify_success_system_message">已验证 %1$s</string>
</resources>
+39 -1
View File
@@ -71,7 +71,7 @@
<string name="cd_remove_favorite">Remove from favorites</string>
<string name="cd_add_bookmark">Add bookmark</string>
<!-- Chat header & accessibility -->
<!-- Chat header &amp; accessibility -->
<string name="chat_back">back</string>
<string name="chat_channel_prefix">channel: %1$s</string>
<string name="chat_leave">leave</string>
@@ -265,6 +265,44 @@
<string name="debug_none">none</string>
<string name="debug_disconnect">disconnect</string>
<string name="debug_recent_scan_results">recent scan results</string>
<string name="verify_title">verify</string>
<string name="verify_my_qr_title">scan to verify me</string>
<string name="verify_scan_prompt_friend">scan someone elses qr</string>
<string name="verify_scan_someone">scan someone elses qr</string>
<string name="verify_show_my_qr">show my qr</string>
<string name="verify_remove">remove verification</string>
<string name="verify_qr_unavailable">qr unavailable</string>
<string name="verify_camera_permission">camera permission is needed to scan qr codes</string>
<string name="verify_request_camera">enable camera</string>
<string name="verify_paste_label">paste verification url</string>
<string name="verify_validate">validate</string>
<string name="verify_scanned">verification requested</string>
<string name="security_verification_title">security verification</string>
<string name="fingerprint_their">their fingerprint</string>
<string name="fingerprint_yours">your fingerprint</string>
<string name="fingerprint_pending">handshake pending</string>
<string name="fingerprint_no_peer">open a private chat to view fingerprints</string>
<string name="fingerprint_status_verified">encrypted &amp; verified</string>
<string name="fingerprint_status_encrypted">encrypted</string>
<string name="fingerprint_status_handshaking">handshaking</string>
<string name="fingerprint_status_failed">handshake failed</string>
<string name="fingerprint_status_uninitialized">not encrypted</string>
<string name="fingerprint_verified_label">verified</string>
<string name="fingerprint_verified_message">you have verified this persons identity.</string>
<string name="fingerprint_not_verified_label">not verified</string>
<string name="fingerprint_not_verified_message_fmt">compare these fingerprints with %1$s using a secure channel.</string>
<string name="fingerprint_mark_verified">mark as verified</string>
<string name="fingerprint_start_handshake">start handshake</string>
<string name="fingerprint_copy">copy</string>
<string name="verify_mutual_match_title">Mutual verification</string>
<string name="verify_mutual_match_body">You and %1$s verified each other</string>
<string name="verify_mutual_system_message">mutual verification with %1$s</string>
<string name="verify_success_title">Verified</string>
<string name="verify_success_body">You verified %1$s</string>
<string name="verify_success_system_message">verified %1$s</string>
<string name="debug_connect">connect</string>
<string name="debug_debug_console">debug console</string>
<string name="debug_clear">clear</string>