update desktop with refactored classes
This commit is contained in:
@@ -42,7 +42,6 @@ import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.containsAny
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import java.math.BigDecimal
|
||||
|
||||
interface UserDependencies
|
||||
@@ -326,23 +325,9 @@ data class RelayInfo(
|
||||
var counter: Long,
|
||||
)
|
||||
|
||||
@Stable
|
||||
class UserBundledRefresherFlow(
|
||||
val user: User,
|
||||
) {
|
||||
val stateFlow = MutableStateFlow(UserState(user))
|
||||
|
||||
fun invalidateData() {
|
||||
stateFlow.tryEmit(UserState(user))
|
||||
}
|
||||
|
||||
fun hasObservers() = stateFlow.subscriptionCount.value > 0
|
||||
}
|
||||
|
||||
@Immutable
|
||||
class UserState(
|
||||
val user: User,
|
||||
)
|
||||
// Re-export from commons.state for backwards compatibility
|
||||
typealias UserBundledRefresherFlow = com.vitorpamplona.amethyst.commons.state.UserMetadataState
|
||||
typealias UserState = com.vitorpamplona.amethyst.commons.state.UserState
|
||||
|
||||
fun Set<User>.toHexSet() = mapTo(LinkedHashSet(size)) { it.pubkeyHex }
|
||||
|
||||
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.state
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
|
||||
/**
|
||||
* Generic event collection state with deduplication, batching, sorting, and size limits.
|
||||
*
|
||||
* Provides efficient management of event/item collections with:
|
||||
* - Automatic deduplication by ID
|
||||
* - Batched updates (250ms default) to reduce recomposition
|
||||
* - Optional sorting via comparator
|
||||
* - Automatic trimming to max size
|
||||
* - Thread-safe operations
|
||||
*
|
||||
* @param T The type of items to collect (must have a unique ID)
|
||||
* @param getId Function to extract unique ID from an item
|
||||
* @param sortComparator Optional comparator for sorting items (null = prepend newest first)
|
||||
* @param maxSize Maximum number of items to keep (older items trimmed)
|
||||
* @param batchDelayMs Delay in milliseconds before flushing batched updates (default 250ms)
|
||||
* @param scope CoroutineScope for batching jobs
|
||||
*
|
||||
* Usage example:
|
||||
* ```
|
||||
* val feedState = EventCollectionState<Event>(
|
||||
* getId = { it.id },
|
||||
* sortComparator = compareByDescending { it.createdAt },
|
||||
* maxSize = 200,
|
||||
* scope = viewModelScope
|
||||
* )
|
||||
*
|
||||
* // Add items (batched automatically)
|
||||
* feedState.addItem(event)
|
||||
* feedState.addItems(eventList)
|
||||
*
|
||||
* // Observe
|
||||
* val items by feedState.items.collectAsState()
|
||||
* ```
|
||||
*/
|
||||
class EventCollectionState<T : Any>(
|
||||
private val getId: (T) -> String,
|
||||
private val sortComparator: Comparator<T>? = null,
|
||||
private val maxSize: Int = 200,
|
||||
private val batchDelayMs: Long = 250,
|
||||
private val scope: CoroutineScope,
|
||||
) {
|
||||
private val _items = MutableStateFlow<List<T>>(emptyList())
|
||||
val items: StateFlow<List<T>> = _items.asStateFlow()
|
||||
|
||||
private val seenIds = mutableSetOf<String>()
|
||||
private val pendingItems = mutableListOf<T>()
|
||||
private val mutex = Mutex()
|
||||
private var batchJob: Job? = null
|
||||
|
||||
/**
|
||||
* Add a single item to the collection.
|
||||
* Updates are batched and applied after batchDelayMs.
|
||||
*
|
||||
* @param item The item to add
|
||||
*/
|
||||
fun addItem(item: T) {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
val itemId = getId(item)
|
||||
if (itemId !in seenIds) {
|
||||
pendingItems.add(item)
|
||||
scheduleBatchUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add multiple items to the collection.
|
||||
* Updates are batched and applied after batchDelayMs.
|
||||
*
|
||||
* @param items The items to add
|
||||
*/
|
||||
fun addItems(items: List<T>) {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
val newItems = items.filter { getId(it) !in seenIds }
|
||||
if (newItems.isNotEmpty()) {
|
||||
pendingItems.addAll(newItems)
|
||||
scheduleBatchUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an item by ID.
|
||||
*
|
||||
* @param id The ID of the item to remove
|
||||
*/
|
||||
fun removeItem(id: String) {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
seenIds.remove(id)
|
||||
_items.value = _items.value.filter { getId(it) != id }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove multiple items by ID.
|
||||
*
|
||||
* @param ids The IDs of items to remove
|
||||
*/
|
||||
fun removeItems(ids: Set<String>) {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
seenIds.removeAll(ids)
|
||||
_items.value = _items.value.filter { getId(it) !in ids }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all items from the collection.
|
||||
*/
|
||||
fun clear() {
|
||||
scope.launch {
|
||||
mutex.withLock {
|
||||
seenIds.clear()
|
||||
pendingItems.clear()
|
||||
_items.value = emptyList()
|
||||
batchJob?.cancel()
|
||||
batchJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current item count.
|
||||
*/
|
||||
val size: Int
|
||||
get() = _items.value.size
|
||||
|
||||
/**
|
||||
* Check if collection is empty.
|
||||
*/
|
||||
val isEmpty: Boolean
|
||||
get() = _items.value.isEmpty()
|
||||
|
||||
/**
|
||||
* Schedules a batched update if not already scheduled.
|
||||
* Cancels existing batch job and starts a new one.
|
||||
*/
|
||||
private fun scheduleBatchUpdate() {
|
||||
batchJob?.cancel()
|
||||
batchJob =
|
||||
scope.launch {
|
||||
delay(batchDelayMs)
|
||||
applyBatchUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies pending items to the collection.
|
||||
* Merges with existing items, sorts if comparator provided, and trims to maxSize.
|
||||
*/
|
||||
private suspend fun applyBatchUpdate() {
|
||||
mutex.withLock {
|
||||
if (pendingItems.isEmpty()) return
|
||||
|
||||
// Add pending IDs to seenIds
|
||||
pendingItems.forEach { seenIds.add(getId(it)) }
|
||||
|
||||
// Merge with existing items
|
||||
val merged = _items.value + pendingItems
|
||||
|
||||
// Sort if comparator provided, otherwise keep newest first (pending items already at end)
|
||||
val sorted =
|
||||
if (sortComparator != null) {
|
||||
merged.sortedWith(sortComparator)
|
||||
} else {
|
||||
// Reverse so newest (pending) items come first
|
||||
(pendingItems.reversed() + _items.value).distinctBy { getId(it) }
|
||||
}
|
||||
|
||||
// Trim to maxSize and update seenIds
|
||||
val trimmed =
|
||||
if (sorted.size > maxSize) {
|
||||
val kept = sorted.take(maxSize)
|
||||
val removed = sorted.drop(maxSize)
|
||||
removed.forEach { seenIds.remove(getId(it)) }
|
||||
kept
|
||||
} else {
|
||||
sorted
|
||||
}
|
||||
|
||||
_items.value = trimmed
|
||||
pendingItems.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force flush pending items immediately without waiting for batch delay.
|
||||
*/
|
||||
suspend fun flush() {
|
||||
batchJob?.cancel()
|
||||
applyBatchUpdate()
|
||||
}
|
||||
}
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.actions.FollowAction
|
||||
import com.vitorpamplona.quartz.nip02FollowList.ContactListEvent
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
/**
|
||||
* Represents the current follow relationship status.
|
||||
*
|
||||
* @property isFollowing Whether the target user is currently followed
|
||||
* @property contactList The current ContactListEvent (NIP-02 kind 3)
|
||||
*/
|
||||
@Immutable
|
||||
data class FollowStatus(
|
||||
val isFollowing: Boolean,
|
||||
val contactList: ContactListEvent?,
|
||||
)
|
||||
|
||||
/**
|
||||
* Reactive state for tracking follow/unfollow status and actions.
|
||||
*
|
||||
* Combines current follow status with action state (loading, success, error)
|
||||
* using LoadingState pattern. Delegates business logic to FollowAction.
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* val followState = FollowState(myPubKeyHex)
|
||||
*
|
||||
* // Update when contact list arrives from relay
|
||||
* followState.updateContactList(contactListEvent, targetPubKeyHex)
|
||||
*
|
||||
* // Follow action
|
||||
* followState.setFollowLoading()
|
||||
* try {
|
||||
* val updated = FollowAction.follow(targetPubKeyHex, signer, contactList)
|
||||
* relayManager.broadcast(updated)
|
||||
* followState.setFollowSuccess(updated, targetPubKeyHex)
|
||||
* } catch (e: Exception) {
|
||||
* followState.setFollowError(e.message ?: "Follow failed")
|
||||
* }
|
||||
*
|
||||
* // Observe in UI
|
||||
* when (val state = followState.state.collectAsState().value) {
|
||||
* is LoadingState.Idle -> { /* Not loaded yet */ }
|
||||
* is LoadingState.Loading -> CircularProgressIndicator()
|
||||
* is LoadingState.Success -> {
|
||||
* val status = state.data
|
||||
* if (status.isFollowing) UnfollowButton() else FollowButton()
|
||||
* }
|
||||
* is LoadingState.Error -> ErrorMessage(state.message)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @property myPubKeyHex The current user's public key hex (for context)
|
||||
*/
|
||||
@Stable
|
||||
class FollowState(
|
||||
private val myPubKeyHex: String,
|
||||
) {
|
||||
private val _state = MutableStateFlow<LoadingState<FollowStatus>>(LoadingState.Idle)
|
||||
val state: StateFlow<LoadingState<FollowStatus>> = _state.asStateFlow()
|
||||
|
||||
/**
|
||||
* Updates the follow status based on a ContactListEvent.
|
||||
*
|
||||
* Checks if targetPubKeyHex is in the contact list's p-tags.
|
||||
*
|
||||
* @param event The ContactListEvent (NIP-02 kind 3)
|
||||
* @param targetPubKeyHex The public key of the user to check
|
||||
*/
|
||||
fun updateContactList(
|
||||
event: ContactListEvent,
|
||||
targetPubKeyHex: String,
|
||||
) {
|
||||
val isFollowing = FollowAction.isFollowing(targetPubKeyHex, event)
|
||||
_state.value = LoadingState.Success(FollowStatus(isFollowing, event))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state to Loading (follow/unfollow action in progress).
|
||||
*
|
||||
* Call this before initiating a follow/unfollow action.
|
||||
*/
|
||||
fun setFollowLoading() {
|
||||
_state.value = LoadingState.Loading
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state to Success with updated follow status.
|
||||
*
|
||||
* Call this after successfully broadcasting a follow/unfollow event.
|
||||
*
|
||||
* @param newContactList The updated ContactListEvent
|
||||
* @param targetPubKeyHex The public key of the user that was followed/unfollowed
|
||||
*/
|
||||
fun setFollowSuccess(
|
||||
newContactList: ContactListEvent,
|
||||
targetPubKeyHex: String,
|
||||
) {
|
||||
val isFollowing = FollowAction.isFollowing(targetPubKeyHex, newContactList)
|
||||
_state.value = LoadingState.Success(FollowStatus(isFollowing, newContactList))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state to Error.
|
||||
*
|
||||
* Call this if follow/unfollow action fails.
|
||||
*
|
||||
* @param message The error message
|
||||
* @param throwable Optional throwable for debugging
|
||||
*/
|
||||
fun setFollowError(
|
||||
message: String,
|
||||
throwable: Throwable? = null,
|
||||
) {
|
||||
_state.value = LoadingState.Error(message, throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current follow status, if loaded.
|
||||
*
|
||||
* @return FollowStatus if state is Success, null otherwise
|
||||
*/
|
||||
fun currentStatusOrNull(): FollowStatus? = state.value.dataOrNull()
|
||||
|
||||
/**
|
||||
* Gets the current ContactListEvent, if loaded.
|
||||
*
|
||||
* @return ContactListEvent if state is Success, null otherwise
|
||||
*/
|
||||
fun currentContactListOrNull(): ContactListEvent? = currentStatusOrNull()?.contactList
|
||||
|
||||
/**
|
||||
* Checks if currently following, if loaded.
|
||||
*
|
||||
* @return true if following, false if not following or not loaded
|
||||
*/
|
||||
fun isFollowing(): Boolean = currentStatusOrNull()?.isFollowing ?: false
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.state
|
||||
|
||||
/**
|
||||
* Generic loading state representation for async operations.
|
||||
*
|
||||
* Provides type-safe state management for loading, success, error, and empty states.
|
||||
* Eliminates the need for multiple boolean flags (isLoading, hasError, etc.).
|
||||
*
|
||||
* @param T The type of data when successfully loaded
|
||||
*
|
||||
* Usage example:
|
||||
* ```
|
||||
* val feedState: StateFlow<LoadingState<List<Event>>> = ...
|
||||
*
|
||||
* when (val state = feedState.collectAsState().value) {
|
||||
* is LoadingState.Idle -> { /* Not started yet */ }
|
||||
* is LoadingState.Loading -> CircularProgressIndicator()
|
||||
* is LoadingState.Success -> LazyColumn { items(state.data) { ... } }
|
||||
* is LoadingState.Error -> ErrorMessage(state.message)
|
||||
* is LoadingState.Empty -> EmptyPlaceholder()
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
sealed class LoadingState<out T> {
|
||||
/**
|
||||
* Initial state - operation has not started yet.
|
||||
* Useful for actions (like follow/unfollow) that haven't been triggered.
|
||||
*/
|
||||
object Idle : LoadingState<Nothing>()
|
||||
|
||||
/**
|
||||
* Operation is in progress.
|
||||
*/
|
||||
object Loading : LoadingState<Nothing>()
|
||||
|
||||
/**
|
||||
* Operation completed successfully with data.
|
||||
*
|
||||
* @param data The loaded data
|
||||
*/
|
||||
data class Success<T>(
|
||||
val data: T,
|
||||
) : LoadingState<T>()
|
||||
|
||||
/**
|
||||
* Operation failed with an error.
|
||||
*
|
||||
* @param message Human-readable error message
|
||||
* @param throwable Optional exception for debugging/logging
|
||||
*/
|
||||
data class Error(
|
||||
val message: String,
|
||||
val throwable: Throwable? = null,
|
||||
) : LoadingState<Nothing>()
|
||||
|
||||
/**
|
||||
* Operation completed successfully but returned no data.
|
||||
* Useful for feeds/lists that have no items.
|
||||
*/
|
||||
object Empty : LoadingState<Nothing>()
|
||||
|
||||
/**
|
||||
* Returns true if this state represents a successful load (Success or Empty).
|
||||
*/
|
||||
val isSuccessful: Boolean
|
||||
get() = this is Success || this is Empty
|
||||
|
||||
/**
|
||||
* Returns true if this state represents a failure.
|
||||
*/
|
||||
val isError: Boolean
|
||||
get() = this is Error
|
||||
|
||||
/**
|
||||
* Returns true if this state is currently loading.
|
||||
*/
|
||||
val isLoading: Boolean
|
||||
get() = this is Loading
|
||||
|
||||
/**
|
||||
* Returns the data if this is a Success state, null otherwise.
|
||||
*/
|
||||
fun dataOrNull(): T? = if (this is Success) data else null
|
||||
|
||||
/**
|
||||
* Returns the error message if this is an Error state, null otherwise.
|
||||
*/
|
||||
fun errorOrNull(): String? = if (this is Error) message else null
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/**
|
||||
* Reactive state wrapper for User metadata.
|
||||
*
|
||||
* Provides a StateFlow that emits UserState when user metadata changes.
|
||||
* Used by both Android ViewModels and Desktop composables for reactive UI updates.
|
||||
*
|
||||
* Android pattern: UserBundledRefresherFlow (typealias for backwards compatibility)
|
||||
*/
|
||||
@Stable
|
||||
class UserMetadataState(
|
||||
val user: User,
|
||||
) {
|
||||
val stateFlow = MutableStateFlow(UserState(user))
|
||||
|
||||
fun invalidateData() {
|
||||
stateFlow.tryEmit(UserState(user))
|
||||
}
|
||||
|
||||
fun hasObservers() = stateFlow.subscriptionCount.value > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable snapshot of User state.
|
||||
*
|
||||
* Emitted by UserMetadataState.stateFlow to trigger recomposition.
|
||||
*/
|
||||
@Immutable
|
||||
class UserState(
|
||||
val user: User,
|
||||
)
|
||||
Reference in New Issue
Block a user