chore: fix some crash and performance issues #45
@@ -98,9 +98,9 @@ import su.reya.coop.RoomKind
|
|||||||
import su.reya.coop.RoomUiState
|
import su.reya.coop.RoomUiState
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
import su.reya.coop.ago
|
import su.reya.coop.ago
|
||||||
|
import su.reya.coop.flow
|
||||||
import su.reya.coop.shared.Avatar
|
import su.reya.coop.shared.Avatar
|
||||||
import su.reya.coop.shared.getExpressiveFontFamily
|
import su.reya.coop.shared.getExpressiveFontFamily
|
||||||
import su.reya.coop.uiStateFlow
|
|
||||||
import su.reya.coop.viewmodel.AccountViewModel
|
import su.reya.coop.viewmodel.AccountViewModel
|
||||||
import su.reya.coop.viewmodel.ChatViewModel
|
import su.reya.coop.viewmodel.ChatViewModel
|
||||||
|
|
||||||
@@ -620,9 +620,9 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
val firstRoom = requests.getOrNull(0)
|
val firstRoom = requests.getOrNull(0)
|
||||||
val secondRoom = requests.getOrNull(1)
|
val secondRoom = requests.getOrNull(1)
|
||||||
|
|
||||||
val firstRoomState by (firstRoom as Room).uiStateFlow(profileCache)
|
val firstRoomState by (firstRoom as Room).flow(profileCache)
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
.collectAsStateWithLifecycle(RoomUiState())
|
||||||
val secondRoomState by (secondRoom ?: firstRoom).uiStateFlow(profileCache)
|
val secondRoomState by (secondRoom ?: firstRoom).flow(profileCache)
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
val supportingText = when {
|
val supportingText = when {
|
||||||
@@ -695,8 +695,7 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||||
val profileCache = LocalProfileCache.current
|
val profileCache = LocalProfileCache.current
|
||||||
val roomState by room.uiStateFlow(profileCache)
|
val roomState by room.flow(profileCache).collectAsStateWithLifecycle(RoomUiState())
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
modifier = Modifier.clickable(onClick = onClick),
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import su.reya.coop.repository.AccountState
|
|||||||
class AccountViewModel(
|
class AccountViewModel(
|
||||||
private val repository: AccountRepository,
|
private val repository: AccountRepository,
|
||||||
) : ViewModel(), ErrorHost by repository {
|
) : ViewModel(), ErrorHost by repository {
|
||||||
|
|
||||||
val state: StateFlow<AccountState> = repository.state
|
val state: StateFlow<AccountState> = repository.state
|
||||||
val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile
|
val isUpdatingProfile: StateFlow<Boolean> = repository.isUpdatingProfile
|
||||||
val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile
|
val currentUserProfile: StateFlow<Profile?> = repository.currentUserProfile
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
package su.reya.coop.viewmodel
|
package su.reya.coop.viewmodel
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import su.reya.coop.Profile
|
import su.reya.coop.Profile
|
||||||
@@ -19,27 +18,41 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
|
|
||||||
class ProfileCache(
|
class ProfileCache(
|
||||||
private val nostr: Nostr,
|
private val nostr: Nostr,
|
||||||
|
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
) : ErrorHost by createErrorHost() {
|
) : ErrorHost by createErrorHost() {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
private val scope = CoroutineScope(SupervisorJob() + defaultDispatcher)
|
||||||
private val profilesMutex = Mutex()
|
private val profiles = MutableStateFlow<Map<PublicKey, MutableStateFlow<Profile?>>>(emptyMap())
|
||||||
private val profiles = mutableMapOf<PublicKey, MutableStateFlow<Profile?>>()
|
private val seenPublicKeys = MutableStateFlow<Set<PublicKey>>(emptySet())
|
||||||
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
||||||
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scope.launch { runObserver() }
|
|
||||||
scope.launch { runMetadataBatching() }
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
nostr.waitUntilInitialized()
|
try {
|
||||||
loadCacheMetadata()
|
runObserver()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Metadata observer failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
runMetadataBatching()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Metadata batching failed: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
nostr.waitUntilInitialized()
|
||||||
|
getCachedMetadata()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showError("Failed to load initial cache: ${e.message}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun runObserver() = coroutineScope {
|
private suspend fun runObserver() {
|
||||||
launch {
|
nostr.profiles.metadataUpdates.collect { (pubkey, metadata) ->
|
||||||
nostr.profiles.metadataUpdates.collect { (pubkey, metadata) ->
|
updateMetadata(pubkey, Profile(pubkey, metadata))
|
||||||
updateMetadata(pubkey, Profile(pubkey, metadata))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,37 +70,69 @@ class ProfileCache(
|
|||||||
batch.add(nextKey)
|
batch.add(nextKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
nostr.profiles.fetchMetadataBatch(batch.toList())
|
try {
|
||||||
}
|
nostr.profiles.fetchMetadataBatch(batch.toList())
|
||||||
}
|
} catch (e: Exception) {
|
||||||
|
// Allow these keys to be requested again since the fetch failed
|
||||||
private suspend fun loadCacheMetadata() {
|
seenPublicKeys.update { it - batch }
|
||||||
val cache = nostr.profiles.getAllCacheMetadata()
|
println("Failed to fetch metadata batch: ${e.message}")
|
||||||
|
|
||||||
profilesMutex.withLock {
|
|
||||||
cache.forEach { (pubkey, metadata) ->
|
|
||||||
val profile = Profile(pubkey, metadata)
|
|
||||||
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
|
|
||||||
seenPublicKeys.add(pubkey)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestMetadata(pubkey: PublicKey) {
|
private suspend fun getCachedMetadata() {
|
||||||
if (seenPublicKeys.add(pubkey)) {
|
val cache = nostr.profiles.getAllCacheMetadata()
|
||||||
metadataRequestChannel.trySend(pubkey)
|
cache.forEach { (pubkey, metadata) ->
|
||||||
|
updateMetadata(pubkey, Profile(pubkey, metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateMetadata(pubkey: PublicKey, profile: Profile) {
|
private fun requestMetadata(pubkey: PublicKey) {
|
||||||
profilesMutex.withLock {
|
var added = false
|
||||||
profiles.getOrPut(pubkey) { MutableStateFlow(null) }.value = profile
|
seenPublicKeys.update { current ->
|
||||||
|
if (current.contains(pubkey)) {
|
||||||
|
added = false
|
||||||
|
current
|
||||||
|
} else {
|
||||||
|
added = true
|
||||||
|
current + pubkey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (added) metadataRequestChannel.trySend(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMetadata(pubkey: PublicKey, profile: Profile) {
|
||||||
|
profiles.update { current ->
|
||||||
|
val flow = current[pubkey] ?: MutableStateFlow<Profile?>(null)
|
||||||
|
flow.value = profile
|
||||||
|
if (current.containsKey(pubkey)) current else current + (pubkey to flow)
|
||||||
|
}
|
||||||
|
seenPublicKeys.update { it + pubkey }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> {
|
fun getMetadata(pubkey: PublicKey): StateFlow<Profile?> {
|
||||||
val flow = profiles.getOrPut(pubkey) { MutableStateFlow(null) }
|
val currentMap = profiles.value
|
||||||
if (flow.value == null) requestMetadata(pubkey)
|
val existingFlow = currentMap[pubkey]
|
||||||
return flow.asStateFlow()
|
|
||||||
|
if (existingFlow != null) {
|
||||||
|
if (existingFlow.value == null) requestMetadata(pubkey)
|
||||||
|
return existingFlow.asStateFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
val newFlow = MutableStateFlow<Profile?>(null)
|
||||||
|
var resultFlow = newFlow
|
||||||
|
|
||||||
|
profiles.update { prev ->
|
||||||
|
if (prev.containsKey(pubkey)) {
|
||||||
|
resultFlow = prev[pubkey]!!
|
||||||
|
prev
|
||||||
|
} else {
|
||||||
|
prev + (pubkey to newFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultFlow.value == null) requestMetadata(pubkey)
|
||||||
|
|
||||||
|
return resultFlow.asStateFlow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user