chore: merge the develop branch into master #1
@@ -1,5 +1,6 @@
|
|||||||
package su.reya.coop.screens
|
package su.reya.coop.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
@@ -75,7 +76,7 @@ fun ChatScreen(
|
|||||||
val groupedMessages = remember(messages.toList()) {
|
val groupedMessages = remember(messages.toList()) {
|
||||||
messages.groupBy { it.createdAt().formatAsGroupHeader() }
|
messages.groupBy { it.createdAt().formatAsGroupHeader() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLoading(value: Boolean) {
|
fun setLoading(value: Boolean) {
|
||||||
loading = value
|
loading = value
|
||||||
}
|
}
|
||||||
@@ -240,7 +241,17 @@ fun ChatMessage(
|
|||||||
color = containerColor,
|
color = containerColor,
|
||||||
contentColor = contentColor,
|
contentColor = contentColor,
|
||||||
shape = bubbleShape,
|
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(
|
||||||
text = rumor.content(),
|
text = rumor.content(),
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ class Nostr {
|
|||||||
private set
|
private set
|
||||||
var msgRelayList: Map<PublicKey, List<RelayUrl>> = emptyMap()
|
var msgRelayList: Map<PublicKey, List<RelayUrl>> = emptyMap()
|
||||||
private set
|
private set
|
||||||
|
var sentEvents: MutableMap<EventId, List<RelayUrl>> = mutableMapOf()
|
||||||
|
private set
|
||||||
|
var rumorMap: MutableMap<EventId, EventId> = mutableMapOf()
|
||||||
|
private set
|
||||||
|
|
||||||
suspend fun init(dbPath: String) {
|
suspend fun init(dbPath: String) {
|
||||||
try {
|
try {
|
||||||
@@ -94,6 +98,7 @@ class Nostr {
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
// Bootstrap relays
|
// Bootstrap relays
|
||||||
|
client?.addRelay(RelayUrl.parse("wss://relay.damus.io"))
|
||||||
client?.addRelay(RelayUrl.parse("wss://relay.primal.net"))
|
client?.addRelay(RelayUrl.parse("wss://relay.primal.net"))
|
||||||
client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
client?.addRelay(RelayUrl.parse("wss://user.kindpag.es"))
|
||||||
client?.addRelay(RelayUrl.parse("wss://purplepag.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 -> {
|
else -> {
|
||||||
/* Ignore other message types */
|
/* Ignore other message types */
|
||||||
}
|
}
|
||||||
@@ -495,7 +507,7 @@ class Nostr {
|
|||||||
|
|
||||||
client?.sendEvent(
|
client?.sendEvent(
|
||||||
event = metadataEvent,
|
event = metadataEvent,
|
||||||
target = SendEventTarget.toNip65(),
|
target = SendEventTarget.broadcast(),
|
||||||
ackPolicy = AckPolicy.none()
|
ackPolicy = AckPolicy.none()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -515,7 +527,7 @@ class Nostr {
|
|||||||
|
|
||||||
suspend fun fetchMetadataBatch(keys: List<PublicKey>) {
|
suspend fun fetchMetadataBatch(keys: List<PublicKey>) {
|
||||||
try {
|
try {
|
||||||
val limit = keys.size.toULong();
|
val limit = keys.size.toULong() * 4u;
|
||||||
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
||||||
|
|
||||||
// Construct a filter for metadata events
|
// Construct a filter for metadata events
|
||||||
@@ -528,8 +540,10 @@ class Nostr {
|
|||||||
val target =
|
val target =
|
||||||
ReqTarget.manual(
|
ReqTarget.manual(
|
||||||
mapOf(
|
mapOf(
|
||||||
|
RelayUrl.parse("wss://purplepag.es") to listOf(filter),
|
||||||
RelayUrl.parse("wss://user.kindpag.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)
|
val events = client?.database()?.query(filter)
|
||||||
|
|
||||||
// Collect rooms
|
// Collect rooms
|
||||||
val rooms: MutableSet<Room> = mutableSetOf()
|
val roomsMap: MutableMap<Long, Room> = mutableMapOf()
|
||||||
|
|
||||||
events
|
events
|
||||||
?.toVec()
|
?.toVec()
|
||||||
?.map { UnsignedEvent.fromJson(it.content()) }
|
?.map { UnsignedEvent.fromJson(it.content()) }
|
||||||
?.filter { it.tags().publicKeys().isNotEmpty() }
|
?.filter { it.tags().publicKeys().isNotEmpty() }
|
||||||
?.sortedByDescending { it.createdAt().asSecs() }
|
|
||||||
?.forEach { event ->
|
?.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
|
// Check if the room already exists
|
||||||
if (rooms.contains(room)) {
|
if (existingRoom == null || newRoom.createdAt.asSecs() > existingRoom.createdAt.asSecs()) {
|
||||||
room.setCreatedAt(room.createdAt)
|
val filter =
|
||||||
room.setLastMessage(room.lastMessage)
|
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) {
|
} catch (e: Exception) {
|
||||||
println("Failed to get chat rooms: ${e.message}")
|
println("Failed to get chat rooms: ${e.message}")
|
||||||
return null
|
return null
|
||||||
@@ -625,7 +633,7 @@ class Nostr {
|
|||||||
content: String,
|
content: String,
|
||||||
subject: String? = null,
|
subject: String? = null,
|
||||||
replies: List<EventId> = emptyList(),
|
replies: List<EventId> = emptyList(),
|
||||||
onNewMessage: ((UnsignedEvent) -> Unit)? = null
|
onRumorCreated: ((UnsignedEvent) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
val currentUser =
|
val currentUser =
|
||||||
@@ -664,7 +672,7 @@ class Nostr {
|
|||||||
|
|
||||||
// Emit the rumor to the chat screen
|
// Emit the rumor to the chat screen
|
||||||
if (receiver == currentUser) {
|
if (receiver == currentUser) {
|
||||||
onNewMessage?.invoke(rumor)
|
onRumorCreated?.invoke(rumor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct the gift wrap event
|
// Construct the gift wrap event
|
||||||
@@ -678,12 +686,19 @@ class Nostr {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Send the event to receiver's NIP-17 relays
|
// Send the event to receiver's NIP-17 relays
|
||||||
client?.sendEvent(
|
val output = client?.sendEvent(
|
||||||
event = gift,
|
event = gift,
|
||||||
target = SendEventTarget.toNip17(),
|
target = SendEventTarget.toNip17(),
|
||||||
ackPolicy = AckPolicy.none(),
|
ackPolicy = AckPolicy.none(),
|
||||||
authenticationTimeout = Duration.parse("2s")
|
authenticationTimeout = Duration.parse("2s")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (output != null) {
|
||||||
|
sentEvents[output.id] = emptyList()
|
||||||
|
if (rumor.id() != null) {
|
||||||
|
rumorMap[rumor.id()!!] = output.id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to send message: ${e.message}", e)
|
throw IllegalStateException("Failed to send message: ${e.message}", e)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import rust.nostr.sdk.Metadata
|
|||||||
import rust.nostr.sdk.NostrConnect
|
import rust.nostr.sdk.NostrConnect
|
||||||
import rust.nostr.sdk.NostrConnectUri
|
import rust.nostr.sdk.NostrConnectUri
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.RelayUrl
|
||||||
import rust.nostr.sdk.Tag
|
import rust.nostr.sdk.Tag
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
import su.reya.coop.blossom.BlossomClient
|
import su.reya.coop.blossom.BlossomClient
|
||||||
@@ -50,6 +51,9 @@ class NostrViewModel(
|
|||||||
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
|
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
|
||||||
val newEvents = _newEvents.asSharedFlow()
|
val newEvents = _newEvents.asSharedFlow()
|
||||||
|
|
||||||
|
private val _sentReports = MutableStateFlow<Map<EventId, List<RelayUrl>>>(emptyMap())
|
||||||
|
val sentReport = _sentReports.asSharedFlow()
|
||||||
|
|
||||||
private val _errorEvents = Channel<String>(Channel.BUFFERED)
|
private val _errorEvents = Channel<String>(Channel.BUFFERED)
|
||||||
val errorEvents = _errorEvents.receiveAsFlow()
|
val errorEvents = _errorEvents.receiveAsFlow()
|
||||||
|
|
||||||
@@ -104,6 +108,7 @@ class NostrViewModel(
|
|||||||
if (batch.size >= 10 || (now - lastFlushTime) >= timeout || nextKey == null) {
|
if (batch.size >= 10 || (now - lastFlushTime) >= timeout || nextKey == null) {
|
||||||
val keysToRequest = batch.toList()
|
val keysToRequest = batch.toList()
|
||||||
batch.clear()
|
batch.clear()
|
||||||
|
|
||||||
nostr.fetchMetadataBatch(keysToRequest)
|
nostr.fetchMetadataBatch(keysToRequest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,11 +371,10 @@ class NostrViewModel(
|
|||||||
content = message,
|
content = message,
|
||||||
subject = room.subject,
|
subject = room.subject,
|
||||||
replies = replies,
|
replies = replies,
|
||||||
onNewMessage = { event ->
|
onRumorCreated = { event ->
|
||||||
viewModelScope.launch {
|
updateRoomList(roomId, event)
|
||||||
_newEvents.emit(event)
|
viewModelScope.launch { _newEvents.emit(event) }
|
||||||
}
|
},
|
||||||
}
|
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
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? {
|
suspend fun searchByAddress(query: String): PublicKey? {
|
||||||
try {
|
try {
|
||||||
return nostr.searchByAddress(query)
|
return nostr.searchByAddress(query)
|
||||||
|
|||||||
@@ -29,14 +29,6 @@ data class Room(
|
|||||||
val kind: RoomKind = RoomKind.default(),
|
val kind: RoomKind = RoomKind.default(),
|
||||||
val lastMessage: String? = null
|
val lastMessage: String? = null
|
||||||
) : Comparable<Room> {
|
) : Comparable<Room> {
|
||||||
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 {
|
override fun compareTo(other: Room): Int {
|
||||||
return this.createdAt.asSecs().compareTo(other.createdAt.asSecs())
|
return this.createdAt.asSecs().compareTo(other.createdAt.asSecs())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user