chore: improve performance #41
@@ -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,
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
try {
|
||||||
val currentUser =
|
val currentUser =
|
||||||
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
|
signer.getPublicKeyAsync() ?: throw IllegalStateException("User not signed in")
|
||||||
|
|
||||||
try {
|
// Get the latest metadata event
|
||||||
val record = getLatestMetadata(currentUser)?.asRecord() ?: MetadataRecord()
|
val record = getLatestMetadata(currentUser)?.asRecord() ?: MetadataRecord()
|
||||||
val newRecord = record.copy(
|
|
||||||
|
// Build a new metadata based on old records
|
||||||
|
val newMetadata = Metadata.fromRecord(
|
||||||
|
record.copy(
|
||||||
displayName = name ?: record.displayName,
|
displayName = name ?: record.displayName,
|
||||||
about = bio ?: record.about,
|
about = bio ?: record.about,
|
||||||
picture = picture ?: record.picture
|
picture = picture ?: record.picture
|
||||||
)
|
)
|
||||||
val newMetadata = Metadata.fromRecord(newRecord)
|
)
|
||||||
val event = EventBuilder.metadata(newMetadata).finalizeAsync(signer)
|
|
||||||
|
|
||||||
|
// 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(),
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user