mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +00:00
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:
co-authored by
GitHub Action
callebtc
parent
d73976537d
commit
c663e8ede0
@@ -91,6 +91,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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,6 +188,112 @@ class SecureIdentityStateManager(private val context: Context) {
|
||||
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.
|
||||
// No timed peer ID rotation is performed here.
|
||||
|
||||
@@ -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
|
||||
@@ -940,6 +952,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
|
||||
*/
|
||||
@@ -1128,6 +1184,10 @@ class BluetoothMeshService(private val context: Context) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,12 @@ class ChatState(
|
||||
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)
|
||||
val selectedLocationChannel: StateFlow<com.bitchat.android.geohash.ChannelID?> = _selectedLocationChannel.asStateFlow()
|
||||
@@ -303,6 +309,14 @@ class ChatState(
|
||||
_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) {
|
||||
@@ -745,6 +861,14 @@ class ChatViewModel(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -217,6 +217,14 @@ class MeshDelegateHandler(
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Aus Favoriten entfernen</string>
|
||||
<string name="cd_add_bookmark">Lesezeichen hinzufügen</string>
|
||||
|
||||
<!-- Chat‑Kopf & Barrierefreiheit -->
|
||||
<!-- Chat‑Kopf & 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">Online‑Geohash‑Kanäle</string>
|
||||
<string name="cd_end_to_end_encryption">Ende‑zu‑Ende‑Verschlüsselung</string>
|
||||
|
||||
<!-- Bilder & Dateien -->
|
||||
<!-- Bilder & 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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Retirer des favoris</string>
|
||||
<string name="cd_add_bookmark">Ajouter un signet</string>
|
||||
|
||||
<!-- En‑tête de chat & accessibilité -->
|
||||
<!-- En‑tête de chat & 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 & 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é & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">पसंदीदा से हटाएँ</string>
|
||||
<string name="cd_add_bookmark">बुकमार्क जोड़ें</string>
|
||||
|
||||
<!-- चैट हेडर & एक्सेसिबिलिटी -->
|
||||
<!-- चैट हेडर & एक्सेसिबिलिटी -->
|
||||
<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 & 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>
|
||||
|
||||
@@ -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 & 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 & 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 & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">お気に入りから削除</string>
|
||||
<string name="cd_add_bookmark">ブックマークに追加</string>
|
||||
|
||||
<!-- チャットヘッダー & アクセシビリティ -->
|
||||
<!-- チャットヘッダー & アクセシビリティ -->
|
||||
<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>
|
||||
|
||||
<!-- 画像 & ファイル -->
|
||||
<!-- 画像 & ファイル -->
|
||||
<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>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">რჩეულებიდან წაშლა</string>
|
||||
<string name="cd_add_bookmark">სანიშნეს დამატება</string>
|
||||
|
||||
<!-- ჩათის სათაური & ხელმისაწვდომობა -->
|
||||
<!-- ჩათის სათაური & ხელმისაწვდომობა -->
|
||||
<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>
|
||||
|
||||
<!-- სურათები & ფაილები -->
|
||||
<!-- სურათები & ფაილები -->
|
||||
<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 & 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>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">즐겨찾기에서 제거</string>
|
||||
<string name="cd_add_bookmark">북마크 추가</string>
|
||||
|
||||
<!-- 채팅 헤더 & 접근성 -->
|
||||
<!-- 채팅 헤더 & 접근성 -->
|
||||
<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>
|
||||
|
||||
<!-- 이미지 & 파일 -->
|
||||
<!-- 이미지 & 파일 -->
|
||||
<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 & 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>
|
||||
|
||||
@@ -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 & 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 & 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 & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">Verwijderen uit favorieten</string>
|
||||
<string name="cd_add_bookmark">Bladwijzer toevoegen</string>
|
||||
|
||||
<!-- Chatkop & toegankelijkheid -->
|
||||
<!-- Chatkop & 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 & 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 & 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 & 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>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">پسندیدہ توں ہٹاؤ</string>
|
||||
<string name="cd_add_bookmark">بُک مارک پاؤ</string>
|
||||
|
||||
<!-- چیٹ ہیڈر & رسائی پذیری -->
|
||||
<!-- چیٹ ہیڈر & رسائی پذیری -->
|
||||
<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>
|
||||
|
||||
<!-- تصویراں & فائلز -->
|
||||
<!-- تصویراں & فائلز -->
|
||||
<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 & 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">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 & 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>
|
||||
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<string name="cd_remove_favorite">เอาออกจากรายการโปรด</string>
|
||||
<string name="cd_add_bookmark">เพิ่มที่คั่นหน้า</string>
|
||||
|
||||
<!-- ส่วนหัวแชท & การช่วยสำหรับการเข้าถึง -->
|
||||
<!-- ส่วนหัวแชท & การช่วยสำหรับการเข้าถึง -->
|
||||
<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>
|
||||
|
||||
<!-- รูปภาพ & ไฟล์ -->
|
||||
<!-- รูปภาพ & ไฟล์ -->
|
||||
<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 & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
|
||||
@@ -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 & 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>
|
||||
|
||||
@@ -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 & 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 & 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 & 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & 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 & 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>
|
||||
|
||||
@@ -45,6 +45,13 @@ gms-location = "21.3.0"
|
||||
# Security
|
||||
security-crypto = "1.1.0-beta01"
|
||||
|
||||
# QR
|
||||
zxing-core = "3.5.4"
|
||||
|
||||
# CameraX / ML Kit
|
||||
camerax = "1.5.2"
|
||||
mlkit-barcode = "17.3.0"
|
||||
|
||||
# Testing
|
||||
junit = "4.13.2"
|
||||
androidx-test-ext = "1.2.1"
|
||||
@@ -104,6 +111,15 @@ gms-location = { module = "com.google.android.gms:play-services-location", versi
|
||||
# Security
|
||||
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" }
|
||||
|
||||
# QR
|
||||
zxing-core = { module = "com.google.zxing:core", version.ref = "zxing-core" }
|
||||
|
||||
# CameraX / ML Kit
|
||||
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
|
||||
androidx-camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
|
||||
androidx-camera-compose = { module = "androidx.camera:camera-compose", version.ref = "camerax" }
|
||||
mlkit-barcode-scanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlkit-barcode" }
|
||||
|
||||
# Testing
|
||||
junit = { module = "junit:junit", version.ref = "junit" }
|
||||
androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext" }
|
||||
|
||||
Reference in New Issue
Block a user