diff --git a/app/src/main/java/com/bitchat/android/services/ContactDirectory.kt b/app/src/main/java/com/bitchat/android/services/ContactDirectory.kt index 5301df6d..b3ffc429 100644 --- a/app/src/main/java/com/bitchat/android/services/ContactDirectory.kt +++ b/app/src/main/java/com/bitchat/android/services/ContactDirectory.kt @@ -26,6 +26,10 @@ object ContactDirectory { @Volatile private var meshProvider: (() -> MeshService?)? = null + @Volatile + internal var identityManagerProvider: (Context) -> SecureIdentityStateManager = + { SecureIdentityStateManager(it) } + fun initialize(context: Context, meshProvider: () -> MeshService?) { appContext = context.applicationContext this.meshProvider = meshProvider @@ -79,7 +83,8 @@ object ContactDirectory { noisePublicKey = noiseKey ?: liveMeshPeerID?.let { meshProvider?.invoke()?.getPeerInfo(it)?.noisePublicKey }, nostrPubkey = favorite?.peerNostrPublicKey, displayName = favorite?.peerNickname?.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) } - ?: liveMeshPeerID?.let { meshProvider?.invoke()?.getPeerInfo(it)?.nickname }, + ?: liveMeshPeerID?.let { meshProvider?.invoke()?.getPeerInfo(it)?.nickname } + ?: contactFingerprint?.let { cachedFingerprintNickname(it) }, isMutualFavorite = favorite?.isMutual == true ) } @@ -132,7 +137,7 @@ object ContactDirectory { private fun cachedNoiseKey(peerID: String): ByteArray? { val context = appContext ?: return null return try { - SecureIdentityStateManager(context) + identityManagerProvider(context) .getCachedNoiseKey(peerID) ?.let { ContactIdentityResolver.bytesFromHex(it) } } catch (_: Exception) { @@ -140,6 +145,17 @@ object ContactDirectory { } } + private fun cachedFingerprintNickname(fingerprint: String): String? { + val context = appContext ?: return null + return try { + identityManagerProvider(context) + .getCachedFingerprintNickname(fingerprint) + ?.takeIf { it.isNotBlank() && !it.equals("Unknown", ignoreCase = true) } + } catch (_: Exception) { + null + } + } + private fun favoriteForMeshPeerID(peerID: String): FavoriteRelationship? = try { FavoritesPersistenceService.shared.getFavoriteStatus(peerID) diff --git a/app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt b/app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt index 28c051bc..8003508e 100644 --- a/app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt +++ b/app/src/main/java/com/bitchat/android/ui/VerificationHandler.kt @@ -8,6 +8,7 @@ import com.bitchat.android.mesh.MeshService import com.bitchat.android.model.BitchatMessage import com.bitchat.android.noise.NoiseSession import com.bitchat.android.nostr.GeohashAliasRegistry +import com.bitchat.android.services.ContactIdentityResolver import com.bitchat.android.services.VerificationService import com.bitchat.android.util.dataFromHexString import com.bitchat.android.util.hexEncodedString @@ -185,6 +186,9 @@ class VerificationHandler( val hexRegex = Regex("^[0-9a-fA-F]+$") return try { when { + ContactIdentityResolver.isContactConversationId(peerID) -> { + ContactIdentityResolver.fingerprintFromContactConversationId(peerID) + } peerID.length == 64 && peerID.matches(hexRegex) -> { identityManager.getCachedNoiseFingerprint(peerID)?.let { return it } fingerprintFromNoiseHex(peerID)?.also { identityManager.cacheNoiseFingerprint(peerID, it) } diff --git a/app/src/test/kotlin/com/bitchat/android/services/ContactDirectoryTest.kt b/app/src/test/kotlin/com/bitchat/android/services/ContactDirectoryTest.kt new file mode 100644 index 00000000..e16996f1 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/services/ContactDirectoryTest.kt @@ -0,0 +1,59 @@ +package com.bitchat.android.services + +import android.content.Context +import android.os.Build +import com.bitchat.android.identity.SecureIdentityStateManager +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config +import java.util.UUID + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [Build.VERSION_CODES.P], manifest = Config.NONE) +class ContactDirectoryTest { + + private lateinit var identityManager: SecureIdentityStateManager + + @Before + fun setup() { + val context = RuntimeEnvironment.getApplication() + val prefs = context.getSharedPreferences( + "contact-directory-test-${UUID.randomUUID()}", + Context.MODE_PRIVATE + ) + identityManager = SecureIdentityStateManager(prefs, testOnly = true) + ContactDirectory.initialize(context) { null } + ContactDirectory.identityManagerProvider = { identityManager } + } + + @After + fun tearDown() { + ContactDirectory.identityManagerProvider = { SecureIdentityStateManager(it) } + } + + @Test + fun `offline contact resolves display name from cached fingerprint nickname`() { + val fingerprint = "ab".repeat(32) + identityManager.cacheFingerprintNickname(fingerprint, "Alice") + + val resolution = ContactDirectory.resolve("contact_$fingerprint") + + assertEquals("Alice", resolution.displayName) + assertNull(resolution.meshPeerID) + } + + @Test + fun `offline contact without cached nickname has no display name`() { + val fingerprint = "cd".repeat(32) + + val resolution = ContactDirectory.resolve("contact_$fingerprint") + + assertNull(resolution.displayName) + } +}