refactor rumor cache
This commit is contained in:
@@ -25,7 +25,7 @@ kotlin {
|
||||
implementation("org.jetbrains.compose.material3:material3:1.11.0-alpha07")
|
||||
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
|
||||
implementation("io.coil-kt.coil3:coil-network-okhttp:3.4.0")
|
||||
implementation("su.reya:nostr-sdk-kmp:0.2.2")
|
||||
implementation("su.reya:nostr-sdk-kmp:0.2.3")
|
||||
}
|
||||
commonMain.dependencies {
|
||||
implementation(libs.compose.runtime)
|
||||
|
||||
@@ -53,7 +53,7 @@ import coop.composeapp.generated.resources.ic_arrow_back
|
||||
import coop.composeapp.generated.resources.ic_avatar
|
||||
import coop.composeapp.generated.resources.ic_send
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
import rust.nostr.sdk.Event
|
||||
import rust.nostr.sdk.UnsignedEvent
|
||||
import su.reya.coop.LocalNostrViewModel
|
||||
import su.reya.coop.LocalSnackbarHostState
|
||||
import su.reya.coop.humanReadable
|
||||
@@ -76,7 +76,7 @@ fun ChatScreen(
|
||||
var text by remember { mutableStateOf("") }
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
|
||||
val messages = remember { mutableStateListOf<Event>() }
|
||||
val messages = remember { mutableStateListOf<UnsignedEvent>() }
|
||||
|
||||
fun setLoading(value: Boolean) {
|
||||
loading = value
|
||||
@@ -177,7 +177,7 @@ fun ChatScreen(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
reverseLayout = true
|
||||
) {
|
||||
items(messages.toList(), key = { it.id().toBech32() }) { event ->
|
||||
items(messages.toList(), key = { it.id()?.toBech32()!! }) { event ->
|
||||
ChatMessage(event)
|
||||
}
|
||||
}
|
||||
@@ -198,11 +198,11 @@ fun ChatScreen(
|
||||
|
||||
@Composable
|
||||
fun ChatMessage(
|
||||
event: Event
|
||||
rumor: UnsignedEvent
|
||||
) {
|
||||
val viewModel = LocalNostrViewModel.current
|
||||
val currentUser = viewModel.currentUser()
|
||||
val isMine = event.author() == currentUser
|
||||
val isMine = rumor.author() == currentUser
|
||||
|
||||
val bubbleShape = if (isMine) {
|
||||
RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp, bottomStart = 20.dp, bottomEnd = 4.dp)
|
||||
@@ -226,7 +226,7 @@ fun ChatMessage(
|
||||
horizontalAlignment = if (isMine) Alignment.End else Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
text = event.createdAt().humanReadable(),
|
||||
text = rumor.createdAt().humanReadable(),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
textAlign = if (isMine) TextAlign.End else TextAlign.Start,
|
||||
)
|
||||
@@ -238,7 +238,7 @@ fun ChatMessage(
|
||||
modifier = Modifier.widthIn(max = 280.dp)
|
||||
) {
|
||||
Text(
|
||||
text = event.content(),
|
||||
text = rumor.content(),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ kotlin {
|
||||
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
||||
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.2")
|
||||
implementation("su.reya:nostr-sdk-kmp:0.2.3")
|
||||
implementation("com.squareup.okio:okio:3.16.2")
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.client.websockets)
|
||||
|
||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import rust.nostr.sdk.AckPolicy
|
||||
import rust.nostr.sdk.Alphabet
|
||||
import rust.nostr.sdk.AsyncNostrSigner
|
||||
import rust.nostr.sdk.Client
|
||||
import rust.nostr.sdk.ClientBuilder
|
||||
@@ -33,6 +34,7 @@ import rust.nostr.sdk.RelayUrl
|
||||
import rust.nostr.sdk.ReqExitPolicy
|
||||
import rust.nostr.sdk.ReqTarget
|
||||
import rust.nostr.sdk.SendEventTarget
|
||||
import rust.nostr.sdk.SingleLetterTag
|
||||
import rust.nostr.sdk.SleepWhenIdle
|
||||
import rust.nostr.sdk.SubscribeAutoCloseOptions
|
||||
import rust.nostr.sdk.Tag
|
||||
@@ -182,8 +184,8 @@ class Nostr {
|
||||
|
||||
suspend fun handleNotifications(
|
||||
onMetadataUpdate: (PublicKey, Metadata) -> Unit,
|
||||
onNewMessage: (UnsignedEvent) -> Unit,
|
||||
onEose: () -> Unit,
|
||||
onNewMessage: (Event) -> Unit
|
||||
) = coroutineScope {
|
||||
val now = Timestamp.now()
|
||||
val processedEvent = mutableSetOf<EventId>()
|
||||
@@ -239,8 +241,7 @@ class Nostr {
|
||||
// Handle new message
|
||||
rumor?.createdAt()?.asSecs()?.let {
|
||||
if (it >= now.asSecs()) {
|
||||
// TODO: only send unsigned event
|
||||
onNewMessage(rumor.signWithKeys(Keys.generate()))
|
||||
onNewMessage(rumor)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -281,7 +282,7 @@ class Nostr {
|
||||
|
||||
private suspend fun getCachedRumor(giftId: EventId): UnsignedEvent? {
|
||||
try {
|
||||
val filter = Filter().identifier(giftId.toBech32())
|
||||
val filter = Filter().identifier(giftId.toHex())
|
||||
val event = client?.database()?.query(filter)?.first()
|
||||
|
||||
return event?.content()?.let { UnsignedEvent.fromJson(it) }
|
||||
@@ -292,18 +293,30 @@ class Nostr {
|
||||
|
||||
private suspend fun setCachedRumor(giftId: EventId, rumor: UnsignedEvent) {
|
||||
try {
|
||||
val rngKeys = Keys.generate()
|
||||
val currentUser =
|
||||
signer.currentUser ?: throw IllegalStateException("User not signed in")
|
||||
|
||||
// Ensure the rumor ID is set
|
||||
val rumor = rumor.ensureId()
|
||||
val roomId = rumor.roomId()
|
||||
|
||||
// Construct a reference event
|
||||
// Construct reference tags
|
||||
val tags = listOf(
|
||||
Tag.identifier(giftId.toHex()),
|
||||
Tag.event(rumor.id()!!),
|
||||
Tag.reference(roomId.toString()),
|
||||
Tag.custom(TagKind.Unknown("k"), listOf("dm"))
|
||||
)
|
||||
|
||||
// Set event kind
|
||||
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA);
|
||||
val tags = listOf(Tag.identifier(giftId.toBech32()), Tag.event(rumor.id()!!))
|
||||
val event = EventBuilder(kind, rumor.asJson()).tags(tags).signWithKeys(rngKeys)
|
||||
|
||||
val event = EventBuilder(kind, rumor.asJson())
|
||||
.tags(tags)
|
||||
.build(currentUser)
|
||||
.signWithKeys(Keys.generate())
|
||||
|
||||
client?.database()?.saveEvent(event)
|
||||
client?.database()?.saveEvent(rumor.signWithKeys(rngKeys))
|
||||
} catch (e: Exception) {
|
||||
println("Failed to set cached rumor: ${e.message}")
|
||||
}
|
||||
@@ -337,19 +350,6 @@ class Nostr {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun conversationId(rumor: UnsignedEvent): Long {
|
||||
val pubkeys: MutableList<PublicKey> = rumor.tags().publicKeys().toMutableList()
|
||||
pubkeys.add(rumor.author())
|
||||
|
||||
val uniqueSortedKeys = pubkeys
|
||||
.map { it.toHex() }
|
||||
.distinct()
|
||||
.sorted()
|
||||
|
||||
return uniqueSortedKeys.hashCode().toLong()
|
||||
}
|
||||
|
||||
|
||||
private suspend fun getDefaultRelayList(): Map<RelayUrl, RelayMetadata> {
|
||||
// Construct a list of relays
|
||||
val relayList = mapOf<RelayUrl, RelayMetadata>(
|
||||
@@ -466,21 +466,19 @@ class Nostr {
|
||||
suspend fun getChatRooms(): Set<Room>? {
|
||||
try {
|
||||
val userPubkey = signer.currentUser ?: throw IllegalStateException("User not signed in")
|
||||
val kind = Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE)
|
||||
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA)
|
||||
val kTag = SingleLetterTag.lowercase(Alphabet.K)
|
||||
|
||||
// Get all events sent by the user
|
||||
val sendFilter = Filter().kind(kind).author(userPubkey)
|
||||
val sendEvents = client?.database()?.query(sendFilter);
|
||||
val filter = Filter().kind(kind).author(userPubkey).customTag(kTag, "dm")
|
||||
val events = client?.database()?.query(filter)
|
||||
|
||||
// Get all events sent to the user
|
||||
val recvFilter = Filter().kind(kind).pubkey(userPubkey)
|
||||
val recvEvents = client?.database()?.query(recvFilter);
|
||||
|
||||
// Collect all events
|
||||
val events = sendEvents?.merge(recvEvents!!)?.toVec();
|
||||
// Collect rooms
|
||||
val rooms: MutableSet<Room> = mutableSetOf()
|
||||
|
||||
events
|
||||
?.toVec()
|
||||
?.map { UnsignedEvent.fromJson(it.content()) }
|
||||
?.filter { it.tags().publicKeys().isNotEmpty() }
|
||||
?.sortedByDescending { it.createdAt().asSecs() }
|
||||
?.forEach { event ->
|
||||
@@ -516,24 +514,17 @@ class Nostr {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChatRoomMessages(members: List<PublicKey>): List<Event> {
|
||||
suspend fun getChatRoomMessages(roomId: Long): List<UnsignedEvent> {
|
||||
try {
|
||||
val userPubkey = signer.currentUser ?: throw IllegalStateException("User not signed in")
|
||||
val kind = Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE)
|
||||
|
||||
val sendFilter = Filter().kind(kind).author(userPubkey).pubkeys(members)
|
||||
val sendEvents = client?.database()?.query(sendFilter)
|
||||
|
||||
val recvFilter = Filter().kind(kind).authors(members).pubkey(userPubkey)
|
||||
val recvEvents = client?.database()?.query(recvFilter)
|
||||
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA)
|
||||
val filter = Filter().kind(kind).reference(roomId.toString())
|
||||
val events = client?.database()?.query(filter)
|
||||
|
||||
// Merge the events
|
||||
val events = sendEvents
|
||||
?.merge(recvEvents!!)
|
||||
return events
|
||||
?.toVec()
|
||||
?.sortedByDescending { it.createdAt().asSecs() }
|
||||
|
||||
return events ?: emptyList()
|
||||
?.map { UnsignedEvent.fromJson(it.content()) }
|
||||
?.sortedByDescending { it.createdAt().asSecs() } ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("Failed to get chat room messages: ${e.message}", e)
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.json.Json
|
||||
import rust.nostr.sdk.Event
|
||||
import rust.nostr.sdk.EventId
|
||||
import rust.nostr.sdk.Keys
|
||||
import rust.nostr.sdk.Metadata
|
||||
import rust.nostr.sdk.NostrConnect
|
||||
import rust.nostr.sdk.NostrConnectUri
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import rust.nostr.sdk.UnsignedEvent
|
||||
import su.reya.coop.blossom.BlossomClient
|
||||
import su.reya.coop.storage.SecretStorage
|
||||
import kotlin.time.Clock
|
||||
@@ -42,7 +42,7 @@ class NostrViewModel(
|
||||
private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet())
|
||||
val chatRooms = _chatRooms.asStateFlow()
|
||||
|
||||
private val _newEvents = MutableSharedFlow<Event>(extraBufferCapacity = 100)
|
||||
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
|
||||
val newEvents = _newEvents.asSharedFlow()
|
||||
|
||||
private val _errorEvents = Channel<String>(Channel.BUFFERED)
|
||||
@@ -302,12 +302,9 @@ class NostrViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getChatRoomMessages(roomId: Long): List<Event> {
|
||||
suspend fun getChatRoomMessages(roomId: Long): List<UnsignedEvent> {
|
||||
try {
|
||||
val room = chatRooms.value.firstOrNull { it.id == roomId } ?: return emptyList()
|
||||
val members = room.members
|
||||
|
||||
return nostr.getChatRoomMessages(members.toList())
|
||||
return nostr.getChatRoomMessages(roomId)
|
||||
} catch (e: Exception) {
|
||||
showError("Error: ${e.message}")
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.minus
|
||||
import kotlinx.datetime.number
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import rust.nostr.sdk.Event
|
||||
import rust.nostr.sdk.PublicKey
|
||||
import rust.nostr.sdk.TagKind
|
||||
import rust.nostr.sdk.Timestamp
|
||||
import rust.nostr.sdk.UnsignedEvent
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
|
||||
@@ -42,7 +42,7 @@ data class Room(
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun new(rumor: Event, userPubkey: PublicKey): Room {
|
||||
fun new(rumor: UnsignedEvent, userPubkey: PublicKey): Room {
|
||||
val id = rumor.roomId()
|
||||
val createdAt = rumor.createdAt()
|
||||
val subject = rumor.tags().find(TagKind.Subject)?.content()
|
||||
@@ -86,7 +86,7 @@ data class Room(
|
||||
}
|
||||
}
|
||||
|
||||
fun Event.roomId(): Long {
|
||||
fun UnsignedEvent.roomId(): Long {
|
||||
// Collect the author's public key and all public keys from tags
|
||||
val pubkeys: MutableList<PublicKey> = mutableListOf()
|
||||
pubkeys.add(this.author())
|
||||
|
||||
Reference in New Issue
Block a user