optimize flow usage

This commit is contained in:
2026-07-10 11:02:10 +07:00
parent 9a8d3c06fa
commit 645430875c
7 changed files with 87 additions and 65 deletions

View File

@@ -33,7 +33,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTooltipState import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -328,7 +327,7 @@ fun ContactListItem(
) { ) {
val nostrViewModel = LocalNostrViewModel.current val nostrViewModel = LocalNostrViewModel.current
val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) } val profileFlow = remember(pubkey) { nostrViewModel.getMetadata(pubkey) }
val profile by profileFlow.collectAsState(initial = null) val profile by profileFlow.collectAsStateWithLifecycle(initialValue = null)
SegmentedListItem( SegmentedListItem(
onClick = onClick, onClick = onClick,

View File

@@ -2,11 +2,13 @@ package su.reya.coop.nostr
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import rust.nostr.sdk.AsyncNostrSigner import rust.nostr.sdk.AsyncNostrSigner
@@ -30,6 +32,7 @@ import rust.nostr.sdk.Timestamp
import rust.nostr.sdk.UnsignedEvent import rust.nostr.sdk.UnsignedEvent
import rust.nostr.sdk.initLogger import rust.nostr.sdk.initLogger
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
object NostrManager { object NostrManager {
val instance = Nostr() val instance = Nostr()
@@ -59,17 +62,18 @@ class Nostr {
private val isInitialized = MutableStateFlow(false) private val isInitialized = MutableStateFlow(false)
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100) private val _newEvents = MutableSharedFlow<UnsignedEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.SUSPEND
)
val newEvents = _newEvents.asSharedFlow() val newEvents = _newEvents.asSharedFlow()
suspend fun emitNewEvent(event: UnsignedEvent) { fun emitNewEvent(event: UnsignedEvent) {
_newEvents.emit(event) _newEvents.tryEmit(event)
} }
suspend fun init( suspend fun init(dbPath: String, logLevel: LogLevel = LogLevel.WARN) {
dbPath: String,
logLevel: LogLevel = LogLevel.WARN
) {
try { try {
if (isInitialized.value) return if (isInitialized.value) return
@@ -105,7 +109,8 @@ class Nostr {
} }
suspend fun waitUntilInitialized() { suspend fun waitUntilInitialized() {
isInitialized.first { it } withTimeoutOrNull(30.seconds) { isInitialized.first { it } }
?: throw IllegalStateException("Nostr initialization timed out")
} }
suspend fun connectBootstrapRelays() { suspend fun connectBootstrapRelays() {

View File

@@ -4,6 +4,7 @@ import io.ktor.client.HttpClient
import io.ktor.client.call.body import io.ktor.client.call.body
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import rust.nostr.sdk.AckPolicy import rust.nostr.sdk.AckPolicy
@@ -33,10 +34,19 @@ class ProfileManager(private val nostr: Nostr) {
private val signer: UniversalSigner get() = nostr.signer private val signer: UniversalSigner get() = nostr.signer
private val _metadataUpdates = private val _metadataUpdates =
MutableSharedFlow<Pair<PublicKey, Metadata>>(extraBufferCapacity = 100) MutableSharedFlow<Pair<PublicKey, Metadata>>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.SUSPEND
)
val metadataUpdates = _metadataUpdates.asSharedFlow() val metadataUpdates = _metadataUpdates.asSharedFlow()
private val _contactListUpdates = MutableSharedFlow<List<PublicKey>>(extraBufferCapacity = 100) private val _contactListUpdates =
MutableSharedFlow<List<PublicKey>>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.SUSPEND
)
val contactListUpdates = _contactListUpdates.asSharedFlow() val contactListUpdates = _contactListUpdates.asSharedFlow()
suspend fun emitMetadataUpdate(pubkey: PublicKey, metadata: Metadata) { suspend fun emitMetadataUpdate(pubkey: PublicKey, metadata: Metadata) {
@@ -138,19 +148,24 @@ class ProfileManager(private val nostr: Nostr) {
bio: String? = null, bio: String? = null,
picture: String? = null picture: String? = null
): Metadata { ): Metadata {
val currentUser =
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
try { try {
val record = getLatestMetadata(currentUser)?.asRecord() ?: MetadataRecord() val currentUser =
val newRecord = record.copy( signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
displayName = name ?: record.displayName,
about = bio ?: record.about,
picture = picture ?: record.picture
)
val newMetadata = Metadata.fromRecord(newRecord)
val event = EventBuilder.metadata(newMetadata).finalizeAsync(signer)
// Get the latest metadata event
val record = getLatestMetadata(currentUser)?.asRecord() ?: MetadataRecord()
// Build a new metadata based on old records
val newMetadata = Metadata.fromRecord(
record.copy(
displayName = name ?: record.displayName,
about = bio ?: record.about,
picture = picture ?: record.picture
)
)
// Send the new metadata event
val event = EventBuilder.metadata(newMetadata).finalizeAsync(signer)
client?.sendEvent( client?.sendEvent(
event = event, event = event,
target = SendEventTarget.broadcast(), target = SendEventTarget.broadcast(),

View File

@@ -104,6 +104,32 @@ class RelayManager(private val nostr: Nostr) {
return msgRelayList return msgRelayList
} }
suspend fun getRelayList(publicKey: PublicKey): Map<RelayUrl, RelayMetadata?> {
try {
val kind = Kind.fromStd(KindStandard.RELAY_LIST)
val filter = Filter().kind(kind).author(publicKey).limit(1u)
val events = client?.database()?.query(filter)
return extractRelayList(events?.toVec()?.firstOrNull() ?: return emptyMap())
} catch (e: Exception) {
throw IllegalStateException("Failed to get relay list: ${e.message}", e)
}
}
suspend fun setRelaylist(relays: Map<RelayUrl, RelayMetadata?>) {
try {
val event = EventBuilder.relayList(relays).finalizeAsync(signer)
client?.sendEvent(
event = event,
target = SendEventTarget.broadcast(),
ackPolicy = AckPolicy.none(),
)
} catch (e: Exception) {
throw IllegalStateException("Failed to set msg relays: ${e.message}", e)
}
}
suspend fun setMsgRelays(urls: List<RelayUrl>) { suspend fun setMsgRelays(urls: List<RelayUrl>) {
try { try {
val event = EventBuilder.nip17RelayList(urls).finalizeAsync(signer) val event = EventBuilder.nip17RelayList(urls).finalizeAsync(signer)
@@ -154,30 +180,4 @@ class RelayManager(private val nostr: Nostr) {
throw IllegalStateException("Failed to fetch msg relays: ${e.message}", e) throw IllegalStateException("Failed to fetch msg relays: ${e.message}", e)
} }
} }
suspend fun getRelayList(publicKey: PublicKey): Map<RelayUrl, RelayMetadata?> {
try {
val kind = Kind.fromStd(KindStandard.RELAY_LIST)
val filter = Filter().kind(kind).author(publicKey).limit(1u)
val events = client?.database()?.query(filter)
return extractRelayList(events?.toVec()?.firstOrNull() ?: return emptyMap())
} catch (e: Exception) {
throw IllegalStateException("Failed to get relay list: ${e.message}", e)
}
}
suspend fun setRelaylist(relays: Map<RelayUrl, RelayMetadata?>) {
try {
val event = EventBuilder.relayList(relays).finalizeAsync(signer)
client?.sendEvent(
event = event,
target = SendEventTarget.broadcast(),
ackPolicy = AckPolicy.none(),
)
} catch (e: Exception) {
throw IllegalStateException("Failed to set msg relays: ${e.message}", e)
}
}
} }

View File

@@ -1,11 +1,16 @@
package su.reya.coop.viewmodel package su.reya.coop.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
abstract class BaseViewModel : ViewModel() { abstract class BaseViewModel : ViewModel() {
private val _errorEvents = MutableSharedFlow<String>(extraBufferCapacity = 10) private val _errorEvents = MutableSharedFlow<String>(
replay = 0,
extraBufferCapacity = 10,
onBufferOverflow = BufferOverflow.SUSPEND
)
val errorEvents = _errorEvents.asSharedFlow() val errorEvents = _errorEvents.asSharedFlow()
protected fun showError(message: String) { protected fun showError(message: String) {

View File

@@ -2,11 +2,11 @@ package su.reya.coop.viewmodel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@@ -16,7 +16,6 @@ import rust.nostr.sdk.EventId
import rust.nostr.sdk.Kind import rust.nostr.sdk.Kind
import rust.nostr.sdk.KindStandard import rust.nostr.sdk.KindStandard
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.Room import su.reya.coop.Room
@@ -26,7 +25,6 @@ import su.reya.coop.roomId
data class ChatState( data class ChatState(
val rooms: Map<Long, Room> = emptyMap(), val rooms: Map<Long, Room> = emptyMap(),
val isSyncing: Boolean = false,
val isPartialProcessedGiftWrap: Boolean = false, val isPartialProcessedGiftWrap: Boolean = false,
) )
@@ -35,25 +33,23 @@ class ChatViewModel(
private val mediaRepository: MediaRepository, private val mediaRepository: MediaRepository,
) : BaseViewModel() { ) : BaseViewModel() {
private val _state = MutableStateFlow(ChatState()) private val _state = MutableStateFlow(ChatState())
val state = combine( val state = _state.stateIn(
_state,
nostr.messages.messageSyncState
) { local, state -> local.copy(isSyncing = state.isSyncing) }.stateIn(
viewModelScope, viewModelScope,
SharingStarted.WhileSubscribed(5000), SharingStarted.WhileSubscribed(5000),
ChatState() ChatState()
) )
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100) private val _newEvents = MutableSharedFlow<UnsignedEvent>(
replay = 0,
extraBufferCapacity = 100,
onBufferOverflow = BufferOverflow.SUSPEND
)
val newEvents = _newEvents.asSharedFlow() val newEvents = _newEvents.asSharedFlow()
private val _sentReports = MutableSharedFlow<Map<EventId, List<RelayUrl>>>()
val sentReport = _sentReports.asSharedFlow()
val chatRooms = state.map { it.rooms.values.sortedByDescending { it.createdAt.asSecs() } } val chatRooms = state.map { it.rooms.values.sortedByDescending { it.createdAt.asSecs() } }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
val isSyncing = state.map { it.isSyncing } val isSyncing = nostr.messages.messageSyncState.map { it.isSyncing }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
val isPartialProcessedGiftWrap = state.map { it.isPartialProcessedGiftWrap } val isPartialProcessedGiftWrap = state.map { it.isPartialProcessedGiftWrap }
@@ -92,7 +88,7 @@ class ChatViewModel(
updateRoomList(roomId, event) updateRoomList(roomId, event)
} }
_newEvents.emit(event) _newEvents.tryEmit(event)
} }
} }
} }
@@ -156,6 +152,8 @@ class ChatViewModel(
suspend fun getChatRoomMessages(roomId: Long): List<UnsignedEvent> { suspend fun getChatRoomMessages(roomId: Long): List<UnsignedEvent> {
try { try {
return nostr.messages.getChatRoomMessages(roomId) return nostr.messages.getChatRoomMessages(roomId)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) { } catch (e: Exception) {
showError("Error: ${e.message}") showError("Error: ${e.message}")
} }
@@ -191,7 +189,7 @@ class ChatViewModel(
replies = replies, replies = replies,
onRumorCreated = { event -> onRumorCreated = { event ->
updateRoomList(roomId, event) updateRoomList(roomId, event)
viewModelScope.launch { _newEvents.emit(event) } viewModelScope.launch { _newEvents.tryEmit(event) }
}, },
) )
} catch (e: Exception) { } catch (e: Exception) {