From 5903de7e827442c23f4d4bb79451e5d73b201d27 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 18 May 2026 17:39:43 +0700 Subject: [PATCH] add check sent message --- .../kotlin/su/reya/coop/screens/ChatScreen.kt | 15 ++++- .../commonMain/kotlin/su/reya/coop/Nostr.kt | 67 ++++++++++++------- .../kotlin/su/reya/coop/NostrViewModel.kt | 35 ++++++++-- .../commonMain/kotlin/su/reya/coop/Room.kt | 8 --- 4 files changed, 84 insertions(+), 41 deletions(-) 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 2db5b00..b945c7e 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -1,5 +1,6 @@ package su.reya.coop.screens +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize @@ -75,7 +76,7 @@ fun ChatScreen( val groupedMessages = remember(messages.toList()) { messages.groupBy { it.createdAt().formatAsGroupHeader() } } - + fun setLoading(value: Boolean) { loading = value } @@ -240,7 +241,17 @@ fun ChatMessage( color = containerColor, contentColor = contentColor, shape = bubbleShape, - modifier = Modifier.widthIn(max = 280.dp) + modifier = Modifier + .widthIn(max = 280.dp) + .clickable( + onClick = { + val id = rumor.id() + if (id != null) { + val sent = viewModel.isMessageSent(id) + println("Sent: $sent") + } + } + ) ) { Text( text = rumor.content(), diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt index 22aad08..20c7e6d 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Nostr.kt @@ -66,6 +66,10 @@ class Nostr { private set var msgRelayList: Map> = emptyMap() private set + var sentEvents: MutableMap> = mutableMapOf() + private set + var rumorMap: MutableMap = mutableMapOf() + private set suspend fun init(dbPath: String) { try { @@ -94,6 +98,7 @@ class Nostr { .build() // Bootstrap relays + client?.addRelay(RelayUrl.parse("wss://relay.damus.io")) client?.addRelay(RelayUrl.parse("wss://relay.primal.net")) client?.addRelay(RelayUrl.parse("wss://user.kindpag.es")) client?.addRelay(RelayUrl.parse("wss://purplepag.es")) @@ -334,6 +339,13 @@ class Nostr { } } + is RelayMessageEnum.Ok -> { + if (sentEvents.containsKey(message.eventId)) { + val currentRelays = sentEvents[message.eventId] ?: emptyList() + sentEvents[message.eventId] = currentRelays + relayUrl + } + } + else -> { /* Ignore other message types */ } @@ -495,7 +507,7 @@ class Nostr { client?.sendEvent( event = metadataEvent, - target = SendEventTarget.toNip65(), + target = SendEventTarget.broadcast(), ackPolicy = AckPolicy.none() ) @@ -515,7 +527,7 @@ class Nostr { suspend fun fetchMetadataBatch(keys: List) { try { - val limit = keys.size.toULong(); + val limit = keys.size.toULong() * 4u; val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose) // Construct a filter for metadata events @@ -528,8 +540,10 @@ class Nostr { val target = ReqTarget.manual( mapOf( + RelayUrl.parse("wss://purplepag.es") to listOf(filter), RelayUrl.parse("wss://user.kindpag.es") to listOf(filter), - RelayUrl.parse("wss://relay.primal.net") to listOf(filter) + RelayUrl.parse("wss://relay.primal.net") to listOf(filter), + RelayUrl.parse("wss://relay.damus.io") to listOf(filter), ) ) @@ -550,37 +564,31 @@ class Nostr { val events = client?.database()?.query(filter) // Collect rooms - val rooms: MutableSet = mutableSetOf() + val roomsMap: MutableMap = mutableMapOf() events ?.toVec() ?.map { UnsignedEvent.fromJson(it.content()) } ?.filter { it.tags().publicKeys().isNotEmpty() } - ?.sortedByDescending { it.createdAt().asSecs() } ?.forEach { event -> - val room = Room.new(rumor = event, userPubkey = userPubkey) + val newRoom = Room.new(rumor = event, userPubkey = userPubkey) + val existingRoom = roomsMap[newRoom.id] // Check if the room already exists - if (rooms.contains(room)) { - room.setCreatedAt(room.createdAt) - room.setLastMessage(room.lastMessage) + if (existingRoom == null || newRoom.createdAt.asSecs() > existingRoom.createdAt.asSecs()) { + val filter = + Filter().kind(kind).author(userPubkey).pubkeys(newRoom.members.toList()) + + // Determine if it's an ongoing room + val isOngoing = client?.database()?.query(filter)?.isEmpty() == false + + // Append room to map + roomsMap[newRoom.id] = + if (isOngoing) newRoom.copy(kind = RoomKind.Ongoing) else newRoom } - - val filter = - Filter().kind(kind).author(userPubkey).pubkeys(room.members.toList()); - - // Check if the user is interacting with the room's members - val isOngoing = client?.database()?.query(filter)?.isEmpty() == false; - - // Set the room kind based on interaction status - if (isOngoing) { - room.setKind(RoomKind.Ongoing) - } - - rooms.add(room) } - return rooms + return roomsMap.values.toSet() } catch (e: Exception) { println("Failed to get chat rooms: ${e.message}") return null @@ -625,7 +633,7 @@ class Nostr { content: String, subject: String? = null, replies: List = emptyList(), - onNewMessage: ((UnsignedEvent) -> Unit)? = null + onRumorCreated: ((UnsignedEvent) -> Unit)? = null, ) { try { val currentUser = @@ -664,7 +672,7 @@ class Nostr { // Emit the rumor to the chat screen if (receiver == currentUser) { - onNewMessage?.invoke(rumor) + onRumorCreated?.invoke(rumor) } // Construct the gift wrap event @@ -678,12 +686,19 @@ class Nostr { ) // Send the event to receiver's NIP-17 relays - client?.sendEvent( + val output = client?.sendEvent( event = gift, target = SendEventTarget.toNip17(), ackPolicy = AckPolicy.none(), authenticationTimeout = Duration.parse("2s") ) + + if (output != null) { + sentEvents[output.id] = emptyList() + if (rumor.id() != null) { + rumorMap[rumor.id()!!] = output.id + } + } } } catch (e: Exception) { throw IllegalStateException("Failed to send message: ${e.message}", e) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt index 840e09c..a680e41 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/NostrViewModel.kt @@ -24,6 +24,7 @@ import rust.nostr.sdk.Metadata import rust.nostr.sdk.NostrConnect import rust.nostr.sdk.NostrConnectUri import rust.nostr.sdk.PublicKey +import rust.nostr.sdk.RelayUrl import rust.nostr.sdk.Tag import rust.nostr.sdk.UnsignedEvent import su.reya.coop.blossom.BlossomClient @@ -50,6 +51,9 @@ class NostrViewModel( private val _newEvents = MutableSharedFlow(extraBufferCapacity = 100) val newEvents = _newEvents.asSharedFlow() + private val _sentReports = MutableStateFlow>>(emptyMap()) + val sentReport = _sentReports.asSharedFlow() + private val _errorEvents = Channel(Channel.BUFFERED) val errorEvents = _errorEvents.receiveAsFlow() @@ -104,6 +108,7 @@ class NostrViewModel( if (batch.size >= 10 || (now - lastFlushTime) >= timeout || nextKey == null) { val keysToRequest = batch.toList() batch.clear() + nostr.fetchMetadataBatch(keysToRequest) } } @@ -366,11 +371,10 @@ class NostrViewModel( content = message, subject = room.subject, replies = replies, - onNewMessage = { event -> - viewModelScope.launch { - _newEvents.emit(event) - } - } + onRumorCreated = { event -> + updateRoomList(roomId, event) + viewModelScope.launch { _newEvents.emit(event) } + }, ) } catch (e: Exception) { showError("Error: ${e.message}") @@ -378,6 +382,27 @@ class NostrViewModel( } } + fun isMessageSent(id: EventId): Boolean { + val giftWrapId = nostr.rumorMap[id] + + if (giftWrapId != null) { + val isSent = nostr.sentEvents[giftWrapId]?.isNotEmpty() ?: false + return isSent + } else { + return false + } + } + + private fun updateRoomList(roomId: Long, newMessage: UnsignedEvent) { + _chatRooms.value = _chatRooms.value.map { room -> + if (room.id == roomId) { + room.copy(lastMessage = newMessage.content(), createdAt = newMessage.createdAt()) + } else { + room + } + }.toSet() + } + suspend fun searchByAddress(query: String): PublicKey? { try { return nostr.searchByAddress(query) diff --git a/shared/src/commonMain/kotlin/su/reya/coop/Room.kt b/shared/src/commonMain/kotlin/su/reya/coop/Room.kt index 72f8e98..3e2ff19 100644 --- a/shared/src/commonMain/kotlin/su/reya/coop/Room.kt +++ b/shared/src/commonMain/kotlin/su/reya/coop/Room.kt @@ -29,14 +29,6 @@ data class Room( val kind: RoomKind = RoomKind.default(), val lastMessage: String? = null ) : Comparable { - override fun hashCode(): Int = id.hashCode() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Room) return false - return id == other.id - } - override fun compareTo(other: Room): Int { return this.createdAt.asSecs().compareTo(other.createdAt.asSecs()) }