mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-10 13:17:34 +01:00
Merge pull request #1499 from KotlinGeekDev/follows-and-followsets-unified
Unification of follows and follow sets.
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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,
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user