diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 52b3eed..3403d27 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -24,7 +24,7 @@ kotlin { implementation(libs.jetbrains.navigation3.ui) implementation(libs.jetbrains.lifecycle.viewmodelNavigation3) implementation(libs.androidx.core.splashscreen) - implementation("su.reya:nostr-sdk-kmp:0.2.6") + implementation("su.reya:nostr-sdk-kmp:0.2.7") implementation("io.coil-kt.coil3:coil-compose:3.4.0") implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0") implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0") diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/AndroidExternalSigner.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/AndroidExternalSigner.kt index e7e3122..3f367fe 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/AndroidExternalSigner.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/AndroidExternalSigner.kt @@ -61,7 +61,7 @@ class AndroidExternalSigner( ): String? { // Try Content Resolver first queryContentResolver(type, payload, pubkey, currentUser)?.let { - return it.result + return if (resultKey == "event") it.event else it.result } // Fall back to Intent diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/ExternalSignerLauncher.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/ExternalSignerLauncher.kt index cbfb686..51bbba7 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/ExternalSignerLauncher.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/ExternalSignerLauncher.kt @@ -4,23 +4,30 @@ import android.content.Intent import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultLauncher import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext class ExternalSignerLauncher { private var launcher: ActivityResultLauncher? = null private var pendingResult: CompletableDeferred? = null + private val mutex = Mutex() fun register(launcher: ActivityResultLauncher) { this.launcher = launcher } - suspend fun launch(intent: Intent): ActivityResult { - val deferred = CompletableDeferred() - pendingResult = deferred - launcher?.launch(intent) - ?: throw IllegalStateException("ExternalSignerLauncher not registered") - return deferred.await() + suspend fun launch(intent: Intent): ActivityResult = mutex.withLock { + withContext(Dispatchers.Main) { + val deferred = CompletableDeferred() + pendingResult = deferred + launcher?.launch(intent) ?: throw IllegalStateException("Signer not registered") + deferred.await() + } } + fun onResult(result: ActivityResult) { pendingResult?.complete(result) pendingResult = null diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index 6c306eb..3e4694f 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -110,17 +110,17 @@ fun ChatScreen(id: Long) { // Start loading spinner loading = true - // Get msg relays for each member - viewModel.chatRoomConnect(id) - // Get messages val initialMessages = viewModel.getChatRoomMessages(id) messages.clear() messages.addAll(initialMessages) - + // Stop loading spinner loading = false + // Get msg relays for each member + viewModel.chatRoomConnect(id) + // Handle new messages viewModel.newEvents.collect { event -> if (event.roomId() == id) { diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 8a5b486..f8e160e 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -33,7 +33,7 @@ kotlin { implementation(libs.androidx.lifecycle.runtimeCompose) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0") - implementation("su.reya:nostr-sdk-kmp:0.2.6") + implementation("su.reya:nostr-sdk-kmp:0.2.7") implementation("com.squareup.okio:okio:3.16.2") } androidMain.dependencies { diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt index 7311152..c029d97 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt @@ -379,27 +379,25 @@ class Nostr { private suspend fun setCachedRumor(giftId: EventId, rumor: UnsignedEvent) { try { - val currentUser = - signer.currentUser ?: throw IllegalStateException("User not signed in") - // Construct the room id val roomId = rumor.roomId() // Construct reference tags val tags = listOf( Tag.identifier(giftId.toHex()), + Tag.publicKey(rumor.author()), Tag.event(rumor.id()!!), - Tag.custom("a", listOf(roomId.toString())), + Tag.custom("r", listOf(roomId.toString())), Tag.custom("k", listOf("14")) ) // Set event kind val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA); + // Construct event val event = EventBuilder(kind, rumor.asJson()) .tags(tags) - .finalizeUnsigned(currentUser) - .signAsync(Keys.generate()) + .finalizeAsync(Keys.generate()) client?.database()?.saveEvent(event) } catch (e: Exception) { @@ -408,27 +406,22 @@ class Nostr { } private suspend fun extractRumor(event: Event): UnsignedEvent? { - // Check if the rumor is already cached - val cachedRumor = getCachedRumor(event.id()) - if (cachedRumor != null) return cachedRumor + try { + // Check if the rumor is already cached + val cachedRumor = getCachedRumor(event.id()) + if (cachedRumor != null) return cachedRumor - // Get all signers - val signers = listOfNotNull(signer, deviceSigner) - if (signers.isEmpty()) return null + // Unwrap the gift with current signer + val gift = UnwrappedGift.fromGiftWrapAsync(signer = signer, giftWrap = event) + val rumor = gift.rumor() - // Try to unwrap the gift with each signer - for (signer in signers) { - try { - val gift = UnwrappedGift.fromGiftWrapAsync(signer = signer, giftWrap = event) - val rumor = gift.rumor() - // Save the rumor to the database - setCachedRumor(event.id(), rumor) - // Return the rumor - return rumor - } catch (e: Exception) { - println("Failed to unwrap gift: ${e.message}") - continue - } + // Save the rumor to the database + setCachedRumor(event.id(), rumor) + + // Return the rumor + return rumor + } catch (e: Exception) { + println("Failed to unwrap gift: ${e.message}") } return null @@ -686,12 +679,14 @@ class Nostr { suspend fun getChatRooms(): Set? { try { - val userPubkey = signer.currentUser ?: throw IllegalStateException("User not signed in") + val userPubkey = + signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in") + val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA) val kTag = SingleLetterTag.lowercase(Alphabet.K) // Get all events sent by the user - val filter = Filter().kind(kind).author(userPubkey).customTags(kTag, listOf("14", "dm")) + val filter = Filter().kind(kind).pubkey(userPubkey).customTags(kTag, listOf("14", "dm")) val events = client?.database()?.query(filter) // Collect rooms @@ -707,8 +702,9 @@ class Nostr { // Check if the room already exists if (existingRoom == null || newRoom.createdAt.asSecs() > existingRoom.createdAt.asSecs()) { - val filter = - Filter().kind(kind).author(userPubkey).pubkeys(newRoom.members.toList()) + val kind = Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE) + val pubkeys = newRoom.members.toList() + val filter = Filter().kind(kind).author(userPubkey).pubkeys(pubkeys) // Determine if it's an ongoing room val isOngoing = client?.database()?.query(filter)?.isEmpty() == false @@ -789,7 +785,7 @@ class Nostr { ) { try { val currentUser = - signer.currentUser ?: throw IllegalStateException("User not signed in") + signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in") val tags = mutableListOf() @@ -816,6 +812,7 @@ class Nostr { val rumor = EventBuilder(Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE), content) .tags(tags) .finalizeUnsigned(currentUser) + .ensureId() // Emit the rumor to the chat screen if (receiver == currentUser) {