Merge pull request #1499 from KotlinGeekDev/follows-and-followsets-unified

Unification of follows and follow sets.
This commit is contained in:
Vitor Pamplona
2025-09-29 13:47:39 -04:00
committed by GitHub
15 changed files with 234 additions and 133 deletions

View File

@@ -57,6 +57,7 @@ import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayLis
import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.blockedRelays.BlockedRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState import com.vitorpamplona.amethyst.model.nip51Lists.broadcastRelays.BroadcastRelayListState
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListDecryptionCache
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListDecryptionCache
@@ -92,7 +93,6 @@ import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler import com.vitorpamplona.amethyst.service.relayClient.reqCommand.nwc.NWCPaymentFilterAssembler
import com.vitorpamplona.amethyst.service.uploads.FileHeader import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor import com.vitorpamplona.amethyst.ui.screen.loggedIn.EventProcessor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
@@ -118,6 +118,7 @@ import com.vitorpamplona.quartz.experimental.profileGallery.mimeType
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.value
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
@@ -214,7 +215,6 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal import java.math.BigDecimal
import java.util.Locale import java.util.Locale
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
@@ -266,6 +266,7 @@ class Account(
val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings) val blockedRelayList = BlockedRelayListState(signer, cache, blockedRelayListDecryptionCache, scope, settings)
val kind3FollowList = FollowListState(signer, cache, scope, settings) val kind3FollowList = FollowListState(signer, cache, scope, settings)
val followSetsState = FollowSetState(signer, cache, scope)
val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer) val ephemeralChatListDecryptionCache = EphemeralChatListDecryptionCache(signer)
val ephemeralChatList = EphemeralChatListState(signer, cache, ephemeralChatListDecryptionCache, scope, settings) val ephemeralChatList = EphemeralChatListState(signer, cache, ephemeralChatListDecryptionCache, scope, settings)
@@ -317,7 +318,7 @@ class Account(
val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow val followsPerRelay = FollowsPerOutboxRelay(kind3FollowList, blockedRelayList, proxyRelayList, cache, scope).flow
// Merges all follow lists to create a single All Follows feed. // Merges all follow lists to create a single All Follows feed.
val allFollows = MergedFollowListsState(kind3FollowList, hashtagList, geohashList, communityList, scope) val allFollows = MergedFollowListsState(kind3FollowList, followSetsState, hashtagList, geohashList, communityList, scope)
val privateDMDecryptionCache = PrivateDMCache(signer) val privateDMDecryptionCache = PrivateDMCache(signer)
val privateZapsDecryptionCache = PrivateZapCache(signer) val privateZapsDecryptionCache = PrivateZapCache(signer)
@@ -829,20 +830,6 @@ class Account(
fun upgradeAttestations() = otsState.upgradeAttestationsIfNeeded(::sendAutomatic) fun upgradeAttestations() = otsState.upgradeAttestationsIfNeeded(::sendAutomatic)
suspend fun getFollowSetNotes() =
withContext(Dispatchers.Default) {
val followSetNotes = LocalCache.getFollowSetNotesFor(userProfile())
Log.d(this@Account.javaClass.simpleName, "Number of follow sets: ${followSetNotes.size}")
return@withContext followSetNotes
}
fun mapNoteToFollowSet(note: Note): FollowSet =
FollowSet
.mapEventToSet(
event = note.event as PeopleListEvent,
signer,
)
suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user)) suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user))
suspend fun unfollow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.unfollow(user)) suspend fun unfollow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.unfollow(user))

View File

@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists package com.vitorpamplona.amethyst.model.nip51Lists.followSets
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.value import com.vitorpamplona.quartz.nip01Core.core.value
@@ -31,9 +31,9 @@ data class FollowSet(
val identifierTag: String, val identifierTag: String,
val title: String, val title: String,
val description: String?, val description: String?,
val visibility: ListVisibility, val visibility: SetVisibility,
val profileList: Set<String>, val profiles: Set<String>,
) : NostrList(listVisibility = visibility, content = profileList) { ) : NostrSet(setVisibility = visibility, content = profiles) {
companion object { companion object {
fun mapEventToSet( fun mapEventToSet(
event: PeopleListEvent, event: PeopleListEvent,
@@ -53,16 +53,16 @@ data class FollowSet(
identifierTag = dTag, identifierTag = dTag,
title = listTitle, title = listTitle,
description = listDescription, description = listDescription,
visibility = ListVisibility.Private, visibility = SetVisibility.Private,
profileList = privateFollows.toSet(), profiles = privateFollows.toSet(),
) )
} else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) { } else if (publicFollows.isNotEmpty() && privateFollows.isEmpty()) {
FollowSet( FollowSet(
identifierTag = dTag, identifierTag = dTag,
title = listTitle, title = listTitle,
description = listDescription, description = listDescription,
visibility = ListVisibility.Public, visibility = SetVisibility.Public,
profileList = publicFollows.toSet(), profiles = publicFollows.toSet(),
) )
} else { } else {
// Follow set is empty, so assume public. Why? Nostr limitation. // Follow set is empty, so assume public. Why? Nostr limitation.
@@ -71,8 +71,8 @@ data class FollowSet(
identifierTag = dTag, identifierTag = dTag,
title = listTitle, title = listTitle,
description = listDescription, description = listDescription,
visibility = ListVisibility.Public, visibility = SetVisibility.Public,
profileList = publicFollows.toSet(), profiles = publicFollows.toSet(),
) )
} }
} }

View File

@@ -0,0 +1,95 @@
/**
* 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.model.nip51Lists.followSets
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FollowSetState(
val signer: NostrSigner,
val cache: LocalCache,
val scope: CoroutineScope,
) {
val user = cache.getOrCreateUser(signer.pubKey)
private val isActive = MutableStateFlow(false)
suspend fun getFollowSetNotes() =
withContext(Dispatchers.Default) {
val followSetNotes = LocalCache.getFollowSetNotesFor(user)
return@withContext followSetNotes
}
private fun getFollowSetNotesFlow() =
flow {
while (isActive.value) {
val followSetNotes = getFollowSetNotes()
val followSets = followSetNotes.map { mapNoteToFollowSet(it) }
emit(followSets)
delay(2000)
}
}.flowOn(Dispatchers.Default)
val profilesFlow =
getFollowSetNotesFlow()
.map { it ->
it.flatMapTo(mutableSetOf()) { it.profiles }.toSet()
}.stateIn(scope, SharingStarted.Eagerly, emptySet())
fun mapNoteToFollowSet(note: Note): FollowSet =
FollowSet
.mapEventToSet(
event = note.event as PeopleListEvent,
signer,
)
fun isUserInFollowSets(user: User): Boolean = profilesFlow.value.contains(user.pubkeyHex)
init {
isActive.update { true }
scope.launch(Dispatchers.Default) {
getFollowSetNotesFlow()
.onCompletion {
isActive.update { false }
}.catch {
Log.e(this@FollowSetState.javaClass.simpleName, "Error on flow collection: ${it.message}")
isActive.update { false }
}.collect {}
}
}
}

View File

@@ -18,15 +18,15 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists package com.vitorpamplona.amethyst.model.nip51Lists.followSets
sealed class NostrList( sealed class NostrSet(
val listVisibility: ListVisibility, val setVisibility: SetVisibility,
val content: Collection<String>, val content: Collection<String>,
) )
class CuratedBookmarkList( class CuratedBookmarkSet(
val name: String, val name: String,
val visibility: ListVisibility, val visibility: SetVisibility,
val listItems: List<String>, val setItems: List<String>,
) : NostrList(visibility, listItems) ) : NostrSet(visibility, setItems)

View File

@@ -18,9 +18,9 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists package com.vitorpamplona.amethyst.model.nip51Lists.followSets
enum class ListVisibility { enum class SetVisibility {
Public, Public,
Private, Private,
Mixed, Mixed,

View File

@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.model.serverList package com.vitorpamplona.amethyst.model.serverList
import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState import com.vitorpamplona.amethyst.model.nip02FollowLists.FollowListState
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState
import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState import com.vitorpamplona.amethyst.model.nip51Lists.geohashLists.GeohashListState
import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState import com.vitorpamplona.amethyst.model.nip51Lists.hashtagLists.HashtagListState
import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState import com.vitorpamplona.amethyst.model.nip72Communities.CommunityListState
@@ -37,6 +38,7 @@ import kotlinx.coroutines.flow.stateIn
class MergedFollowListsState( class MergedFollowListsState(
val kind3List: FollowListState, val kind3List: FollowListState,
val followSetList: FollowSetState,
val hashtagList: HashtagListState, val hashtagList: HashtagListState,
val geohashList: GeohashListState, val geohashList: GeohashListState,
val communityList: CommunityListState, val communityList: CommunityListState,
@@ -44,12 +46,13 @@ class MergedFollowListsState(
) { ) {
fun mergeLists( fun mergeLists(
kind3: FollowListState.Kind3Follows, kind3: FollowListState.Kind3Follows,
followSetProfiles: Set<String>,
hashtags: Set<String>, hashtags: Set<String>,
geohashes: Set<String>, geohashes: Set<String>,
community: Set<CommunityTag>, community: Set<CommunityTag>,
): FollowListState.Kind3Follows = ): FollowListState.Kind3Follows =
FollowListState.Kind3Follows( FollowListState.Kind3Follows(
kind3.authors, kind3.authors + followSetProfiles,
kind3.authorsPlusMe, kind3.authorsPlusMe,
kind3.hashtags + hashtags, kind3.hashtags + hashtags,
kind3.geotags + geohashes, kind3.geotags + geohashes,
@@ -59,15 +62,17 @@ class MergedFollowListsState(
val flow: StateFlow<FollowListState.Kind3Follows> = val flow: StateFlow<FollowListState.Kind3Follows> =
combine( combine(
kind3List.flow, kind3List.flow,
followSetList.profilesFlow,
hashtagList.flow, hashtagList.flow,
geohashList.flow, geohashList.flow,
communityList.flow, communityList.flow,
) { kind3, hashtag, geohash, community -> ) { kind3, followSet, hashtag, geohash, community ->
mergeLists(kind3, hashtag, geohash, community) mergeLists(kind3, followSet, hashtag, geohash, community)
}.onStart { }.onStart {
emit( emit(
mergeLists( mergeLists(
kind3List.flow.value, kind3List.flow.value,
followSetList.profilesFlow.value,
hashtagList.flow.value, hashtagList.flow.value,
geohashList.flow.value, geohashList.flow.value,
communityList.flow.value, communityList.flow.value,

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.relayClient.reqCommand.user
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
@@ -411,6 +412,10 @@ fun observeUserIsFollowing(
): State<Boolean> { ): State<Boolean> {
// Subscribe in the relay for changes in the metadata of this user. // Subscribe in the relay for changes in the metadata of this user.
UserFinderFilterAssemblerSubscription(user1, accountViewModel) UserFinderFilterAssemblerSubscription(user1, accountViewModel)
val isUserInFollowSets =
remember(accountViewModel.account.followSetsState) {
accountViewModel.account.followSetsState.isUserInFollowSets(user2)
}
// Subscribe in the LocalCache for changes that arrive in the device // Subscribe in the LocalCache for changes that arrive in the device
val flow = val flow =
@@ -420,12 +425,14 @@ fun observeUserIsFollowing(
.follows.stateFlow .follows.stateFlow
.sample(1000) .sample(1000)
.mapLatest { userState -> .mapLatest { userState ->
userState.user.isFollowing(user2) userState.user.isFollowing(user2) || isUserInFollowSets
}.distinctUntilChanged() }.distinctUntilChanged()
.flowOn(Dispatchers.Default) .flowOn(Dispatchers.Default)
} }
return flow.collectAsStateWithLifecycle(user1.isFollowing(user2)) return flow.collectAsStateWithLifecycle(
user1.isFollowing(user2) || isUserInFollowSets,
)
} }
@SuppressLint("StateFlowValueCalledInComposition") @SuppressLint("StateFlowValueCalledInComposition")

View File

@@ -21,20 +21,20 @@
package com.vitorpamplona.amethyst.ui.dal package com.vitorpamplona.amethyst.ui.dal
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSetState
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
class FollowSetFeedFilter( class FollowSetFeedFilter(
val account: Account, val followSetState: FollowSetState,
) : FeedFilter<FollowSet>() { ) : FeedFilter<FollowSet>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-followsets" override fun feedKey(): String = followSetState.user.pubkeyHex + "-followsets"
override fun feed(): List<FollowSet> = override fun feed(): List<FollowSet> =
runBlocking(account.scope.coroutineContext) { runBlocking(followSetState.scope.coroutineContext) {
try { try {
val fetchedSets = account.getFollowSetNotes() val fetchedSets = followSetState.getFollowSetNotes()
val followSets = fetchedSets.map { account.mapNoteToFollowSet(it) } val followSets = fetchedSets.map { followSetState.mapNoteToFollowSet(it) }
println("Updated follow set size for feed filter: ${followSets.size}") println("Updated follow set size for feed filter: ${followSets.size}")
followSets followSets
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -59,6 +59,8 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.Route
@@ -76,10 +78,10 @@ fun ListsAndSetsScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val followSetsViewModel: NostrUserListFeedViewModel = val followSetsViewModel: FollowSetFeedViewModel =
viewModel( viewModel(
key = "NostrUserListFeedViewModel", key = "FollowSetFeedViewModel",
factory = NostrUserListFeedViewModel.Factory(accountViewModel.account), factory = FollowSetFeedViewModel.Factory(accountViewModel.account),
) )
ListsAndSetsScreen( ListsAndSetsScreen(
@@ -91,7 +93,7 @@ fun ListsAndSetsScreen(
@Composable @Composable
fun ListsAndSetsScreen( fun ListsAndSetsScreen(
followSetsViewModel: NostrUserListFeedViewModel, followSetsViewModel: FollowSetFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
@@ -117,8 +119,8 @@ fun ListsAndSetsScreen(
refresh = { refresh = {
followSetsViewModel.invalidateData() followSetsViewModel.invalidateData()
}, },
addItem = { title: String, description: String?, listType: ListVisibility -> addItem = { title: String, description: String?, listType: SetVisibility ->
val isSetPrivate = listType == ListVisibility.Private val isSetPrivate = listType == SetVisibility.Private
followSetsViewModel.addFollowSet( followSetsViewModel.addFollowSet(
setName = title, setName = title,
setDescription = description, setDescription = description,
@@ -149,9 +151,9 @@ fun ListsAndSetsScreen(
@Composable @Composable
fun CustomListsScreen( fun CustomListsScreen(
followSetState: FollowSetState, followSetFeedState: FollowSetFeedState,
refresh: () -> Unit, refresh: () -> Unit,
addItem: (title: String, description: String?, listType: ListVisibility) -> Unit, addItem: (title: String, description: String?, listType: SetVisibility) -> Unit,
openItem: (identifier: String) -> Unit, openItem: (identifier: String) -> Unit,
renameItem: (followSet: FollowSet, newName: String) -> Unit, renameItem: (followSet: FollowSet, newName: String) -> Unit,
deleteItem: (followSet: FollowSet) -> Unit, deleteItem: (followSet: FollowSet) -> Unit,
@@ -195,10 +197,10 @@ fun CustomListsScreen(
// TODO: Show components based on current tab // TODO: Show components based on current tab
FollowSetFabsAndMenu( FollowSetFabsAndMenu(
onAddPrivateSet = { name: String, description: String? -> onAddPrivateSet = { name: String, description: String? ->
addItem(name, description, ListVisibility.Private) addItem(name, description, SetVisibility.Private)
}, },
onAddPublicSet = { name: String, description: String? -> onAddPublicSet = { name: String, description: String? ->
addItem(name, description, ListVisibility.Public) addItem(name, description, SetVisibility.Public)
}, },
) )
}, },
@@ -216,7 +218,7 @@ fun CustomListsScreen(
when (page) { when (page) {
0 -> 0 ->
FollowSetFeedView( FollowSetFeedView(
followSetState = followSetState, followSetFeedState = followSetFeedState,
onRefresh = refresh, onRefresh = refresh,
onOpenItem = openItem, onOpenItem = openItem,
onRenameItem = renameItem, onRenameItem = renameItem,
@@ -410,7 +412,7 @@ private fun SetItemPreview() {
identifierTag = "00001-2222", identifierTag = "00001-2222",
title = "Sample List Title", title = "Sample List Title",
description = "Sample List Description", description = "Sample List Description",
visibility = ListVisibility.Mixed, visibility = SetVisibility.Mixed,
emptySet(), emptySet(),
) )
ThemeComparisonColumn { ThemeComparisonColumn {

View File

@@ -55,6 +55,8 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
@@ -98,7 +100,7 @@ fun CustomSetItem(
selected = true, selected = true,
onClick = {}, onClick = {},
label = { label = {
Text(text = "${followSet.profileList.size}") Text(text = "${followSet.profiles.size}")
}, },
leadingIcon = { leadingIcon = {
Icon( Icon(
@@ -121,9 +123,9 @@ fun CustomSetItem(
followSet.visibility.let { followSet.visibility.let {
val text by derivedStateOf { val text by derivedStateOf {
when (it) { when (it) {
ListVisibility.Public -> stringRes(context, R.string.follow_set_type_public) SetVisibility.Public -> stringRes(context, R.string.follow_set_type_public)
ListVisibility.Private -> stringRes(context, R.string.follow_set_type_private) SetVisibility.Private -> stringRes(context, R.string.follow_set_type_private)
ListVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed) SetVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed)
} }
} }
Column( Column(
@@ -135,9 +137,9 @@ fun CustomSetItem(
painter = painter =
painterResource( painterResource(
when (it) { when (it) {
ListVisibility.Public -> R.drawable.ic_public SetVisibility.Public -> R.drawable.ic_public
ListVisibility.Private -> R.drawable.lock SetVisibility.Private -> R.drawable.lock
ListVisibility.Mixed -> R.drawable.format_list_bulleted_type SetVisibility.Mixed -> R.drawable.format_list_bulleted_type
}, },
), ),
contentDescription = stringRes(R.string.follow_set_type_description, text), contentDescription = stringRes(R.string.follow_set_type_description, text),

View File

@@ -20,16 +20,18 @@
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists
sealed class FollowSetState { import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
data object Loading : FollowSetState()
sealed class FollowSetFeedState {
data object Loading : FollowSetFeedState()
data class Loaded( data class Loaded(
val feed: List<FollowSet>, val feed: List<FollowSet>,
) : FollowSetState() ) : FollowSetFeedState()
data object Empty : FollowSetState() data object Empty : FollowSetFeedState()
data class FeedError( data class FeedError(
val errorMessage: String, val errorMessage: String,
) : FollowSetState() ) : FollowSetFeedState()
} }

View File

@@ -35,6 +35,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
import com.vitorpamplona.amethyst.ui.feeds.FeedError import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
@@ -46,17 +47,17 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@Composable @Composable
fun FollowSetFeedView( fun FollowSetFeedView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
followSetState: FollowSetState, followSetFeedState: FollowSetFeedState,
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
onOpenItem: (String) -> Unit = {}, onOpenItem: (String) -> Unit = {},
onRenameItem: (targetSet: FollowSet, newName: String) -> Unit, onRenameItem: (targetSet: FollowSet, newName: String) -> Unit,
onDeleteItem: (followSet: FollowSet) -> Unit, onDeleteItem: (followSet: FollowSet) -> Unit,
) { ) {
when (followSetState) { when (followSetFeedState) {
FollowSetState.Loading -> LoadingFeed() FollowSetFeedState.Loading -> LoadingFeed()
is FollowSetState.Loaded -> { is FollowSetFeedState.Loaded -> {
val followSetFeed = followSetState.feed val followSetFeed = followSetFeedState.feed
FollowSetLoaded( FollowSetLoaded(
loadedFeedState = followSetFeed, loadedFeedState = followSetFeed,
onRefresh = onRefresh, onRefresh = onRefresh,
@@ -66,7 +67,7 @@ fun FollowSetFeedView(
) )
} }
is FollowSetState.Empty -> { is FollowSetFeedState.Empty -> {
FollowSetFeedEmpty( FollowSetFeedEmpty(
message = stringRes(R.string.follow_set_empty_feed_msg), message = stringRes(R.string.follow_set_empty_feed_msg),
) { ) {
@@ -74,9 +75,9 @@ fun FollowSetFeedView(
} }
} }
is FollowSetState.FeedError -> is FollowSetFeedState.FeedError ->
FeedError( FeedError(
followSetState.errorMessage, followSetFeedState.errorMessage,
) { ) {
onRefresh() onRefresh()
} }

View File

@@ -30,6 +30,8 @@ import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter
@@ -49,12 +51,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.UUID import java.util.UUID
// TODO Update: Rename this to be used only for follow sets, and create separate VMs for bookmark sets, etc. class FollowSetFeedViewModel(
class NostrUserListFeedViewModel(
val dataSource: FeedFilter<FollowSet>, val dataSource: FeedFilter<FollowSet>,
) : ViewModel(), ) : ViewModel(),
InvalidatableContent { InvalidatableContent {
private val _feedContent = MutableStateFlow<FollowSetState>(FollowSetState.Loading) private val _feedContent = MutableStateFlow<FollowSetFeedState>(FollowSetFeedState.Loading)
val feedContent = _feedContent.asStateFlow() val feedContent = _feedContent.asStateFlow()
fun refresh() { fun refresh() {
@@ -67,9 +68,8 @@ class NostrUserListFeedViewModel(
noteIdentifier: String, noteIdentifier: String,
account: Account, account: Account,
): AddressableNote? { ): AddressableNote? {
// checkNotInMainThread()
val potentialNote = val potentialNote =
runBlocking(Dispatchers.IO) { account.getFollowSetNotes() } runBlocking(Dispatchers.IO) { account.followSetsState.getFollowSetNotes() }
.find { it.dTag() == noteIdentifier } .find { it.dTag() == noteIdentifier }
return potentialNote return potentialNote
} }
@@ -79,7 +79,7 @@ class NostrUserListFeedViewModel(
account: Account, account: Account,
): Boolean { ): Boolean {
val potentialNote = val potentialNote =
runBlocking(viewModelScope.coroutineContext) { account.getFollowSetNotes() } runBlocking(viewModelScope.coroutineContext) { account.followSetsState.getFollowSetNotes() }
.find { (it.event as PeopleListEvent).nameOrTitle() == setName } .find { (it.event as PeopleListEvent).nameOrTitle() == setName }
return potentialNote != null return potentialNote != null
} }
@@ -94,7 +94,7 @@ class NostrUserListFeedViewModel(
val newSets = dataSource.loadTop().toImmutableList() val newSets = dataSource.loadTop().toImmutableList()
if (oldFeedState is FollowSetState.Loaded) { if (oldFeedState is FollowSetFeedState.Loaded) {
val oldFeedList = oldFeedState.feed.toImmutableList() val oldFeedList = oldFeedState.feed.toImmutableList()
// Using size as a proxy for has changed. // Using size as a proxy for has changed.
if (!equalImmutableLists(newSets, oldFeedList)) { if (!equalImmutableLists(newSets, oldFeedList)) {
@@ -108,7 +108,7 @@ class NostrUserListFeedViewModel(
this.javaClass.simpleName, this.javaClass.simpleName,
"refreshSuspended: Error loading or refreshing feed -> ${e.message}", "refreshSuspended: Error loading or refreshing feed -> ${e.message}",
) )
_feedContent.update { FollowSetState.FeedError(e.message.toString()) } _feedContent.update { FollowSetFeedState.FeedError(e.message.toString()) }
} finally { } finally {
isRefreshing.value = false isRefreshing.value = false
} }
@@ -190,7 +190,7 @@ class NostrUserListFeedViewModel(
PeopleListEvent.addUser( PeopleListEvent.addUser(
earlierVersion = followSetEvent, earlierVersion = followSetEvent,
pubKeyHex = userProfileHex, pubKeyHex = userProfileHex,
isPrivate = followSet.visibility == ListVisibility.Private, isPrivate = followSet.visibility == SetVisibility.Private,
signer = account.signer, signer = account.signer,
) { ) {
account.sendMyPublicAndPrivateOutbox(it) account.sendMyPublicAndPrivateOutbox(it)
@@ -223,9 +223,9 @@ class NostrUserListFeedViewModel(
private fun updateFeed(sets: ImmutableList<FollowSet>) { private fun updateFeed(sets: ImmutableList<FollowSet>) {
if (sets.isNotEmpty()) { if (sets.isNotEmpty()) {
_feedContent.update { FollowSetState.Loaded(sets) } _feedContent.update { FollowSetFeedState.Loaded(sets) }
} else { } else {
_feedContent.update { FollowSetState.Empty } _feedContent.update { FollowSetFeedState.Empty }
} }
} }
@@ -244,7 +244,7 @@ class NostrUserListFeedViewModel(
init { init {
Log.d("Init", this.javaClass.simpleName) Log.d("Init", this.javaClass.simpleName)
Log.d(this.javaClass.simpleName, " FollowSetState : ${_feedContent.value}") Log.d(this.javaClass.simpleName, " FollowSetFeedState : ${_feedContent.value}")
collectorJob = collectorJob =
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
LocalCache.live.newEventBundles.collect { newNotes -> LocalCache.live.newEventBundles.collect { newNotes ->
@@ -266,8 +266,8 @@ class NostrUserListFeedViewModel(
val account: Account, val account: Account,
) : ViewModelProvider.Factory { ) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = override fun <T : ViewModel> create(modelClass: Class<T>): T =
NostrUserListFeedViewModel( FollowSetFeedViewModel(
FollowSetFeedFilter(account), FollowSetFeedFilter(account.followSetsState),
) as T ) as T
} }
} }

View File

@@ -67,14 +67,14 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.FollowSet
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
import com.vitorpamplona.amethyst.ui.components.ClickableBox import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon import com.vitorpamplona.amethyst.ui.note.VerticalDotsIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.BackButton
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
@@ -92,10 +92,10 @@ fun FollowSetScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navigator: INav, navigator: INav,
) { ) {
val followSetViewModel: NostrUserListFeedViewModel = val followSetViewModel: FollowSetFeedViewModel =
viewModel( viewModel(
key = "NostrUserListFeedViewModel", key = "FollowSetFeedViewModel",
factory = NostrUserListFeedViewModel.Factory(accountViewModel.account), factory = FollowSetFeedViewModel.Factory(accountViewModel.account),
) )
FollowSetScreen(selectedSetIdentifier, followSetViewModel, accountViewModel, navigator) FollowSetScreen(selectedSetIdentifier, followSetViewModel, accountViewModel, navigator)
@@ -105,7 +105,7 @@ fun FollowSetScreen(
@Composable @Composable
fun FollowSetScreen( fun FollowSetScreen(
selectedSetIdentifier: String, selectedSetIdentifier: String,
followSetViewModel: NostrUserListFeedViewModel, followSetViewModel: FollowSetFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navigator: INav, navigator: INav,
) { ) {
@@ -144,7 +144,7 @@ fun FollowSetScreen(
when { when {
selectedSetState.value != null -> { selectedSetState.value != null -> {
val selectedSet = selectedSetState.value val selectedSet = selectedSetState.value
val users = selectedSet!!.profileList.mapToUsers(accountViewModel).filterNotNull() val users = selectedSet!!.profiles.mapToUsers(accountViewModel).filterNotNull()
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
@@ -235,10 +235,10 @@ fun TitleAndDescription(
Icon( Icon(
painter = painter =
painterResource( painterResource(
when (followSet.listVisibility) { when (followSet.setVisibility) {
ListVisibility.Public -> R.drawable.ic_public SetVisibility.Public -> R.drawable.ic_public
ListVisibility.Private -> R.drawable.lock SetVisibility.Private -> R.drawable.lock
ListVisibility.Mixed -> R.drawable.format_list_bulleted_type SetVisibility.Mixed -> R.drawable.format_list_bulleted_type
}, },
), ),
contentDescription = null, contentDescription = null,

View File

@@ -78,13 +78,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.nip51Lists.followSets.SetVisibility
import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetState import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedState
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NewSetCreationDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NewSetCreationDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel
import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@@ -97,10 +97,10 @@ fun FollowSetsManagementDialog(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
navigator: INav, navigator: INav,
) { ) {
val followSetViewModel: NostrUserListFeedViewModel = val followSetViewModel: FollowSetFeedViewModel =
viewModel( viewModel(
key = "NostrUserListFeedViewModel", key = "FollowSetFeedViewModel",
factory = NostrUserListFeedViewModel.Factory(accountViewModel.account), factory = FollowSetFeedViewModel.Factory(accountViewModel.account),
) )
FollowSetsManagementDialog(userHex, followSetViewModel, accountViewModel.account, navigator) FollowSetsManagementDialog(userHex, followSetViewModel, accountViewModel.account, navigator)
@@ -110,7 +110,7 @@ fun FollowSetsManagementDialog(
@Composable @Composable
fun FollowSetsManagementDialog( fun FollowSetsManagementDialog(
userHex: String, userHex: String,
followSetsViewModel: NostrUserListFeedViewModel, followSetsViewModel: FollowSetFeedViewModel,
account: Account, account: Account,
navigator: INav, navigator: INav,
) { ) {
@@ -164,17 +164,17 @@ fun FollowSetsManagementDialog(
.imePadding(), .imePadding(),
) { ) {
when (followSetsState) { when (followSetsState) {
is FollowSetState.Loaded -> { is FollowSetFeedState.Loaded -> {
val lists = (followSetsState as FollowSetState.Loaded).feed val lists = (followSetsState as FollowSetFeedState.Loaded).feed
lists.forEachIndexed { index, list -> lists.forEachIndexed { index, list ->
Spacer(StdVertSpacer) Spacer(StdVertSpacer)
FollowSetItem( FollowSetItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
listHeader = list.title, listHeader = list.title,
listVisibility = list.visibility, setVisibility = list.visibility,
userName = userInfo.toBestDisplayName(), userName = userInfo.toBestDisplayName(),
isUserInList = list.profileList.contains(userHex), isUserInList = list.profiles.contains(userHex),
onRemoveUser = { onRemoveUser = {
Log.d( Log.d(
"Amethyst", "Amethyst",
@@ -187,7 +187,7 @@ fun FollowSetsManagementDialog(
) )
Log.d( Log.d(
"Amethyst", "Amethyst",
"Updated List. New size: ${list.profileList.size}", "Updated List. New size: ${list.profiles.size}",
) )
}, },
onAddUser = { onAddUser = {
@@ -198,28 +198,28 @@ fun FollowSetsManagementDialog(
followSetsViewModel.addUserToSet(userHex, list, account) followSetsViewModel.addUserToSet(userHex, list, account)
Log.d( Log.d(
"Amethyst", "Amethyst",
"Updated List. New size: ${list.profileList.size}", "Updated List. New size: ${list.profiles.size}",
) )
}, },
) )
} }
} }
FollowSetState.Empty -> { FollowSetFeedState.Empty -> {
EmptyOrNoneFound { followSetsViewModel.refresh() } EmptyOrNoneFound { followSetsViewModel.refresh() }
} }
is FollowSetState.FeedError -> { is FollowSetFeedState.FeedError -> {
val errorMsg = (followSetsState as FollowSetState.FeedError).errorMessage val errorMsg = (followSetsState as FollowSetFeedState.FeedError).errorMessage
ErrorMessage(errorMsg) { followSetsViewModel.refresh() } ErrorMessage(errorMsg) { followSetsViewModel.refresh() }
} }
FollowSetState.Loading -> { FollowSetFeedState.Loading -> {
Loading() Loading()
} }
} }
if (followSetsState != FollowSetState.Loading) { if (followSetsState != FollowSetFeedState.Loading) {
FollowSetsCreationMenu( FollowSetsCreationMenu(
userName = userInfo.toBestDisplayName(), userName = userInfo.toBestDisplayName(),
onSetCreate = { setName, setIsPrivate, description -> onSetCreate = { setName, setIsPrivate, description ->
@@ -304,7 +304,7 @@ private fun ErrorMessage(
fun FollowSetItem( fun FollowSetItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
listHeader: String, listHeader: String,
listVisibility: ListVisibility, setVisibility: SetVisibility,
userName: String, userName: String,
isUserInList: Boolean, isUserInList: Boolean,
onAddUser: () -> Unit, onAddUser: () -> Unit,
@@ -330,21 +330,21 @@ fun FollowSetItem(
) { ) {
Text(listHeader, fontWeight = FontWeight.Bold) Text(listHeader, fontWeight = FontWeight.Bold)
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
listVisibility.let { setVisibility.let {
val text by derivedStateOf { val text by derivedStateOf {
when (it) { when (it) {
ListVisibility.Public -> stringRes(context, R.string.follow_set_type_public) SetVisibility.Public -> stringRes(context, R.string.follow_set_type_public)
ListVisibility.Private -> stringRes(context, R.string.follow_set_type_private) SetVisibility.Private -> stringRes(context, R.string.follow_set_type_private)
ListVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed) SetVisibility.Mixed -> stringRes(context, R.string.follow_set_type_mixed)
} }
} }
Icon( Icon(
painter = painter =
painterResource( painterResource(
when (listVisibility) { when (setVisibility) {
ListVisibility.Public -> R.drawable.ic_public SetVisibility.Public -> R.drawable.ic_public
ListVisibility.Private -> R.drawable.lock SetVisibility.Private -> R.drawable.lock
ListVisibility.Mixed -> R.drawable.format_list_bulleted_type SetVisibility.Mixed -> R.drawable.format_list_bulleted_type
}, },
), ),
contentDescription = stringRes(R.string.follow_set_type_description, text), contentDescription = stringRes(R.string.follow_set_type_description, text),