fix: nostr operations cause app crashing (#20)
Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
@@ -24,7 +24,7 @@ kotlin {
|
|||||||
implementation(libs.jetbrains.navigation3.ui)
|
implementation(libs.jetbrains.navigation3.ui)
|
||||||
implementation(libs.jetbrains.lifecycle.viewmodelNavigation3)
|
implementation(libs.jetbrains.lifecycle.viewmodelNavigation3)
|
||||||
implementation(libs.androidx.core.splashscreen)
|
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-compose:3.4.0")
|
||||||
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
|
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
|
||||||
implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0")
|
implementation("io.github.kalinjul.easyqrscan:scanner:0.7.0")
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class AndroidExternalSigner(
|
|||||||
): String? {
|
): String? {
|
||||||
// Try Content Resolver first
|
// Try Content Resolver first
|
||||||
queryContentResolver(type, payload, pubkey, currentUser)?.let {
|
queryContentResolver(type, payload, pubkey, currentUser)?.let {
|
||||||
return it.result
|
return if (resultKey == "event") it.event else it.result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to Intent
|
// Fall back to Intent
|
||||||
|
|||||||
@@ -4,22 +4,29 @@ import android.content.Intent
|
|||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ExternalSignerLauncher {
|
class ExternalSignerLauncher {
|
||||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||||
private var pendingResult: CompletableDeferred<ActivityResult>? = null
|
private var pendingResult: CompletableDeferred<ActivityResult>? = null
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
fun register(launcher: ActivityResultLauncher<Intent>) {
|
fun register(launcher: ActivityResultLauncher<Intent>) {
|
||||||
this.launcher = launcher
|
this.launcher = launcher
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun launch(intent: Intent): ActivityResult {
|
suspend fun launch(intent: Intent): ActivityResult = mutex.withLock {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
val deferred = CompletableDeferred<ActivityResult>()
|
val deferred = CompletableDeferred<ActivityResult>()
|
||||||
pendingResult = deferred
|
pendingResult = deferred
|
||||||
launcher?.launch(intent)
|
launcher?.launch(intent) ?: throw IllegalStateException("Signer not registered")
|
||||||
?: throw IllegalStateException("ExternalSignerLauncher not registered")
|
deferred.await()
|
||||||
return deferred.await()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun onResult(result: ActivityResult) {
|
fun onResult(result: ActivityResult) {
|
||||||
pendingResult?.complete(result)
|
pendingResult?.complete(result)
|
||||||
|
|||||||
@@ -110,9 +110,6 @@ fun ChatScreen(id: Long) {
|
|||||||
// Start loading spinner
|
// Start loading spinner
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
// Get msg relays for each member
|
|
||||||
viewModel.chatRoomConnect(id)
|
|
||||||
|
|
||||||
// Get messages
|
// Get messages
|
||||||
val initialMessages = viewModel.getChatRoomMessages(id)
|
val initialMessages = viewModel.getChatRoomMessages(id)
|
||||||
messages.clear()
|
messages.clear()
|
||||||
@@ -121,6 +118,9 @@ fun ChatScreen(id: Long) {
|
|||||||
// Stop loading spinner
|
// Stop loading spinner
|
||||||
loading = false
|
loading = false
|
||||||
|
|
||||||
|
// Get msg relays for each member
|
||||||
|
viewModel.chatRoomConnect(id)
|
||||||
|
|
||||||
// Handle new messages
|
// Handle new messages
|
||||||
viewModel.newEvents.collect { event ->
|
viewModel.newEvents.collect { event ->
|
||||||
if (event.roomId() == id) {
|
if (event.roomId() == id) {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ kotlin {
|
|||||||
implementation(libs.androidx.lifecycle.runtimeCompose)
|
implementation(libs.androidx.lifecycle.runtimeCompose)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0")
|
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")
|
implementation("com.squareup.okio:okio:3.16.2")
|
||||||
}
|
}
|
||||||
androidMain.dependencies {
|
androidMain.dependencies {
|
||||||
|
|||||||
@@ -379,27 +379,25 @@ class Nostr {
|
|||||||
|
|
||||||
private suspend fun setCachedRumor(giftId: EventId, rumor: UnsignedEvent) {
|
private suspend fun setCachedRumor(giftId: EventId, rumor: UnsignedEvent) {
|
||||||
try {
|
try {
|
||||||
val currentUser =
|
|
||||||
signer.currentUser ?: throw IllegalStateException("User not signed in")
|
|
||||||
|
|
||||||
// Construct the room id
|
// Construct the room id
|
||||||
val roomId = rumor.roomId()
|
val roomId = rumor.roomId()
|
||||||
|
|
||||||
// Construct reference tags
|
// Construct reference tags
|
||||||
val tags = listOf(
|
val tags = listOf(
|
||||||
Tag.identifier(giftId.toHex()),
|
Tag.identifier(giftId.toHex()),
|
||||||
|
Tag.publicKey(rumor.author()),
|
||||||
Tag.event(rumor.id()!!),
|
Tag.event(rumor.id()!!),
|
||||||
Tag.custom("a", listOf(roomId.toString())),
|
Tag.custom("r", listOf(roomId.toString())),
|
||||||
Tag.custom("k", listOf("14"))
|
Tag.custom("k", listOf("14"))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set event kind
|
// Set event kind
|
||||||
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA);
|
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA);
|
||||||
|
|
||||||
|
// Construct event
|
||||||
val event = EventBuilder(kind, rumor.asJson())
|
val event = EventBuilder(kind, rumor.asJson())
|
||||||
.tags(tags)
|
.tags(tags)
|
||||||
.finalizeUnsigned(currentUser)
|
.finalizeAsync(Keys.generate())
|
||||||
.signAsync(Keys.generate())
|
|
||||||
|
|
||||||
client?.database()?.saveEvent(event)
|
client?.database()?.saveEvent(event)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -408,27 +406,22 @@ class Nostr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun extractRumor(event: Event): UnsignedEvent? {
|
private suspend fun extractRumor(event: Event): UnsignedEvent? {
|
||||||
|
try {
|
||||||
// Check if the rumor is already cached
|
// Check if the rumor is already cached
|
||||||
val cachedRumor = getCachedRumor(event.id())
|
val cachedRumor = getCachedRumor(event.id())
|
||||||
if (cachedRumor != null) return cachedRumor
|
if (cachedRumor != null) return cachedRumor
|
||||||
|
|
||||||
// Get all signers
|
// Unwrap the gift with current signer
|
||||||
val signers = listOfNotNull(signer, deviceSigner)
|
|
||||||
if (signers.isEmpty()) return null
|
|
||||||
|
|
||||||
// Try to unwrap the gift with each signer
|
|
||||||
for (signer in signers) {
|
|
||||||
try {
|
|
||||||
val gift = UnwrappedGift.fromGiftWrapAsync(signer = signer, giftWrap = event)
|
val gift = UnwrappedGift.fromGiftWrapAsync(signer = signer, giftWrap = event)
|
||||||
val rumor = gift.rumor()
|
val rumor = gift.rumor()
|
||||||
|
|
||||||
// Save the rumor to the database
|
// Save the rumor to the database
|
||||||
setCachedRumor(event.id(), rumor)
|
setCachedRumor(event.id(), rumor)
|
||||||
|
|
||||||
// Return the rumor
|
// Return the rumor
|
||||||
return rumor
|
return rumor
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to unwrap gift: ${e.message}")
|
println("Failed to unwrap gift: ${e.message}")
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -686,12 +679,14 @@ class Nostr {
|
|||||||
|
|
||||||
suspend fun getChatRooms(): Set<Room>? {
|
suspend fun getChatRooms(): Set<Room>? {
|
||||||
try {
|
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 kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA)
|
||||||
val kTag = SingleLetterTag.lowercase(Alphabet.K)
|
val kTag = SingleLetterTag.lowercase(Alphabet.K)
|
||||||
|
|
||||||
// Get all events sent by the user
|
// 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)
|
val events = client?.database()?.query(filter)
|
||||||
|
|
||||||
// Collect rooms
|
// Collect rooms
|
||||||
@@ -707,8 +702,9 @@ class Nostr {
|
|||||||
|
|
||||||
// Check if the room already exists
|
// Check if the room already exists
|
||||||
if (existingRoom == null || newRoom.createdAt.asSecs() > existingRoom.createdAt.asSecs()) {
|
if (existingRoom == null || newRoom.createdAt.asSecs() > existingRoom.createdAt.asSecs()) {
|
||||||
val filter =
|
val kind = Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE)
|
||||||
Filter().kind(kind).author(userPubkey).pubkeys(newRoom.members.toList())
|
val pubkeys = newRoom.members.toList()
|
||||||
|
val filter = Filter().kind(kind).author(userPubkey).pubkeys(pubkeys)
|
||||||
|
|
||||||
// Determine if it's an ongoing room
|
// Determine if it's an ongoing room
|
||||||
val isOngoing = client?.database()?.query(filter)?.isEmpty() == false
|
val isOngoing = client?.database()?.query(filter)?.isEmpty() == false
|
||||||
@@ -789,7 +785,7 @@ class Nostr {
|
|||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val currentUser =
|
val currentUser =
|
||||||
signer.currentUser ?: throw IllegalStateException("User not signed in")
|
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
|
||||||
|
|
||||||
val tags = mutableListOf<Tag>()
|
val tags = mutableListOf<Tag>()
|
||||||
|
|
||||||
@@ -816,6 +812,7 @@ class Nostr {
|
|||||||
val rumor = EventBuilder(Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE), content)
|
val rumor = EventBuilder(Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE), content)
|
||||||
.tags(tags)
|
.tags(tags)
|
||||||
.finalizeUnsigned(currentUser)
|
.finalizeUnsigned(currentUser)
|
||||||
|
.ensureId()
|
||||||
|
|
||||||
// Emit the rumor to the chat screen
|
// Emit the rumor to the chat screen
|
||||||
if (receiver == currentUser) {
|
if (receiver == currentUser) {
|
||||||
|
|||||||
Reference in New Issue
Block a user