Add other functions and adapt them to the code already present.

This commit is contained in:
KotlinGeekDev
2025-08-06 18:56:15 +01:00
parent 7f8b2a6ccf
commit 981486f09a
12 changed files with 324 additions and 283 deletions

View File

@@ -90,6 +90,7 @@ import com.vitorpamplona.amethyst.model.torState.TorRelayState
import com.vitorpamplona.amethyst.service.location.LocationState
import com.vitorpamplona.amethyst.service.ots.OkHttpOtsResolverBuilder
import com.vitorpamplona.amethyst.service.uploads.FileHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet
import com.vitorpamplona.quartz.experimental.bounties.BountyAddValueEvent
import com.vitorpamplona.quartz.experimental.edits.TextNoteModificationEvent
import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryBaseEvent
@@ -215,6 +216,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.util.Locale
@@ -839,6 +841,23 @@ class Account(
}
}
suspend fun getFollowSetNotes() =
withContext(Dispatchers.Default) {
val followSetNotes = LocalCache.getFollowSetNotesFor(userProfile())
userProfile().updateFollowSetNotes(followSetNotes)
// userProfile().followSets = followSetNotes
println("Number of follow sets: ${followSetNotes.size}")
}
suspend fun mapNoteToFollowSet(note: Note): FollowSet =
FollowSet
.mapEventToSet(
event = note.event as PeopleListEvent,
signer,
)
// fun followSetNotesFlow() = MutableStateFlow(userProfile().followSets)
suspend fun updateAttestations() = sendAutomatic(otsState.updateAttestations())
suspend fun follow(user: User) = sendMyPublicAndPrivateOutbox(kind3FollowList.follow(user))
@@ -1609,60 +1628,6 @@ class Account(
suspend fun hideUser(pubkeyHex: HexKey) {
sendMyPublicAndPrivateOutbox(muteList.hideUser(pubkeyHex))
fun getAppSpecificDataNote() = LocalCache.getOrCreateAddressableNote(AppSpecificDataEvent.createAddress(userProfile().pubkeyHex, APP_SPECIFIC_DATA_D_TAG))
fun getAppSpecificDataFlow(): StateFlow<NoteState> = getAppSpecificDataNote().flow().metadata.stateFlow
fun getBlockListNote() = LocalCache.getOrCreateAddressableNote(PeopleListEvent.createBlockAddress(userProfile().pubkeyHex))
fun getMuteListNote() = LocalCache.getOrCreateAddressableNote(MuteListEvent.createAddress(userProfile().pubkeyHex))
suspend fun getFollowSetNotes() =
withContext(Dispatchers.Default) {
val followSetNotes = LocalCache.getFollowSetNotesFor(userProfile())
userProfile().updateFollowSetNotes(followSetNotes)
// userProfile().followSets = followSetNotes
println("Number of follow sets: ${followSetNotes.size}")
}
fun mapNoteToFollowSet(note: Note): FollowSet =
FollowSet
.mapEventToSet(
event = note.event as PeopleListEvent,
signer,
)
// fun followSetNotesFlow() = MutableStateFlow(userProfile().followSets)
fun getMuteListFlow(): StateFlow<NoteState> = getMuteListNote().flow().metadata.stateFlow
fun getBlockList(): PeopleListEvent? = getBlockListNote().event as? PeopleListEvent
fun getMuteList(): MuteListEvent? = getMuteListNote().event as? MuteListEvent
fun hideWord(word: String) {
val muteList = getMuteList()
if (muteList != null) {
MuteListEvent.addWord(
earlierVersion = muteList,
word = word,
isPrivate = true,
signer = signer,
) {
Amethyst.instance.client.send(it)
LocalCache.consume(it, null)
}
} else {
MuteListEvent.createListWithWord(
word = word,
isPrivate = true,
signer = signer,
) {
Amethyst.instance.client.send(it)
LocalCache.consume(it, null)
}
}
}
suspend fun showUser(pubkeyHex: HexKey) {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -32,9 +32,10 @@ class FollowSetFeedFilter(
override fun feedKey(): String = account.userProfile().pubkeyHex
override fun feed(): List<FollowSet> {
val followSetCache = mutableListOf<FollowSet>()
account.scope.launch {
val userFollowSets = account.userProfile().followSetNotes
if (userFollowSets.isEmpty()) {
account.scope.launch {
try {
account.getFollowSetNotes()
} catch (e: Exception) {
@@ -43,8 +44,10 @@ class FollowSetFeedFilter(
null
}
}
userFollowSets.map { account.mapNoteToFollowSet(it) }.forEach {
followSetCache.add(it)
}
val followSets = userFollowSets.map { account.mapNoteToFollowSet(it) }
return followSets
}
return followSetCache.toList()
}
}

View File

@@ -23,116 +23,19 @@ package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
import com.vitorpamplona.amethyst.ui.dal.NIP90ContentDiscoveryResponseFilter
import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.FeedContentState
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrListFeedViewModel
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class NostrChannelFeedViewModel(
val channel: Channel,
val account: Account,
) : FeedViewModel(ChannelFeedFilter(channel, account)) {
class Factory(
val channel: Channel,
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrChannelFeedViewModel : ViewModel> create(modelClass: Class<NostrChannelFeedViewModel>): NostrChannelFeedViewModel = NostrChannelFeedViewModel(channel, account) as NostrChannelFeedViewModel
}
}
class NostrChatroomFeedViewModel(
val user: ChatroomKey,
val account: Account,
) : FeedViewModel(ChatroomFeedFilter(user, account)) {
class Factory(
val user: ChatroomKey,
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrChatRoomFeedViewModel : ViewModel> create(modelClass: Class<NostrChatRoomFeedViewModel>): NostrChatRoomFeedViewModel = NostrChatroomFeedViewModel(user, account) as NostrChatRoomFeedViewModel
}
}
class NostrThreadFeedViewModel(
account: Account,
noteId: String,
) : LevelFeedViewModel(ThreadFeedFilter(account, noteId)) {
class Factory(
val account: Account,
val noteId: String,
) : ViewModelProvider.Factory {
override fun <NostrThreadFeedViewModel : ViewModel> create(modelClass: Class<NostrThreadFeedViewModel>): NostrThreadFeedViewModel = NostrThreadFeedViewModel(account, noteId) as NostrThreadFeedViewModel
}
}
class NostrHashtagFeedViewModel(
val hashtag: String,
val account: Account,
) : FeedViewModel(HashtagFeedFilter(hashtag, account)) {
class Factory(
val hashtag: String,
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrHashtagFeedViewModel : ViewModel> create(modelClass: Class<NostrHashtagFeedViewModel>): NostrHashtagFeedViewModel = NostrHashtagFeedViewModel(hashtag, account) as NostrHashtagFeedViewModel
}
}
class NostrGeoHashFeedViewModel(
val geohash: String,
val account: Account,
) : FeedViewModel(GeoHashFeedFilter(geohash, account)) {
class Factory(
val geohash: String,
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrGeoHashFeedViewModel : ViewModel> create(modelClass: Class<NostrGeoHashFeedViewModel>): NostrGeoHashFeedViewModel = NostrGeoHashFeedViewModel(geohash, account) as NostrGeoHashFeedViewModel
}
}
class NostrCommunityFeedViewModel(
val note: AddressableNote,
val account: Account,
) : FeedViewModel(CommunityFeedFilter(note, account)) {
class Factory(
val note: AddressableNote,
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrCommunityFeedViewModel : ViewModel> create(modelClass: Class<NostrCommunityFeedViewModel>): NostrCommunityFeedViewModel = NostrCommunityFeedViewModel(note, account) as NostrCommunityFeedViewModel
}
}
@Stable
class NostrBookmarkPublicFeedViewModel(
val account: Account,
) : FeedViewModel(BookmarkPublicFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrBookmarkPublicFeedViewModel : ViewModel> create(modelClass: Class<NostrBookmarkPublicFeedViewModel>): NostrBookmarkPublicFeedViewModel = NostrBookmarkPublicFeedViewModel(account) as NostrBookmarkPublicFeedViewModel
}
}
@Stable
class NostrBookmarkPrivateFeedViewModel(
val account: Account,
) : FeedViewModel(BookmarkPrivateFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrBookmarkPrivateFeedViewModel : ViewModel> create(modelClass: Class<NostrBookmarkPrivateFeedViewModel>): NostrBookmarkPrivateFeedViewModel = NostrBookmarkPrivateFeedViewModel(account) as NostrBookmarkPrivateFeedViewModel
}
}
@Stable
class NostrUserListFeedViewModel(
val account: Account,
@@ -144,82 +47,6 @@ class NostrUserListFeedViewModel(
}
}
@Stable
class NostrNIP90ContentDiscoveryFeedViewModel(
val account: Account,
dvmkey: String,
requestid: String,
) : FeedViewModel(NIP90ContentDiscoveryResponseFilter(account, dvmkey, requestid)) {
class Factory(
val account: Account,
val dvmkey: String,
val requestid: String,
) : ViewModelProvider.Factory {
override fun <NostrNIP90ContentDiscoveryFeedViewModel : ViewModel> create(modelClass: Class<NostrNIP90ContentDiscoveryFeedViewModel>): NostrNIP90ContentDiscoveryFeedViewModel = NostrNIP90ContentDiscoveryFeedViewModel(account, dvmkey, requestid) as NostrNIP90ContentDiscoveryFeedViewModel
}
}
@Stable
class NostrDraftEventsFeedViewModel(
val account: Account,
) : FeedViewModel(DraftEventsFeedFilter(account)) {
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <NostrDraftEventsFeedViewModel : ViewModel> create(modelClass: Class<NostrDraftEventsFeedViewModel>): NostrDraftEventsFeedViewModel = NostrDraftEventsFeedViewModel(account) as NostrDraftEventsFeedViewModel
}
}
abstract class LevelFeedViewModel(
localFilter: FeedFilter<Note>,
) : FeedViewModel(localFilter) {
var llState: LazyListState by mutableStateOf(LazyListState(0, 0))
val hasDragged = mutableStateOf(false)
val selectedIDHex =
llState.interactionSource.interactions
.onEach {
if (it is DragInteraction.Start) {
hasDragged.value = true
}
}.stateIn(
viewModelScope,
SharingStarted.Eagerly,
null,
)
@OptIn(ExperimentalCoroutinesApi::class)
val levelCacheFlow: StateFlow<Map<Note, Int>> =
feedState.feedContent
.transformLatest { feed ->
emitAll(
if (feed is FeedState.Loaded) {
feed.feed.map {
val cache = mutableMapOf<Note, Int>()
it.list.forEach {
ThreadLevelCalculator.replyLevel(it, cache)
}
cache
}
} else {
MutableStateFlow(mapOf())
},
)
}.flowOn(Dispatchers.Default)
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
mapOf(),
)
fun levelFlowForItem(note: Note) =
levelCacheFlow
.map {
it[note] ?: 0
}.distinctUntilChanged()
}
@Stable
abstract class FeedViewModel(
localFilter: FeedFilter<Note>,

View File

@@ -177,25 +177,9 @@ class AccountViewModel(
var firstRoute: Route? = null
// TODO: contact lists are not notes yet
// val kind3Relays: StateFlow<ContactListEvent?> = observeByAuthor(ContactListEvent.KIND, account.signer.pubKey)
val toastManager = ToastManager()
val normalizedKind3RelaySetFlow =
account
.userProfile()
.flow()
.relays.stateFlow
.map { contactListState ->
checkNotInMainThread()
contactListState.user.latestContactList?.relays()?.map {
RelayUrlFormatter.normalize(it.key)
} ?: emptySet()
}.flowOn(Dispatchers.Default)
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(10000, 10000),
emptySet(),
)
val feedStates = AccountFeedContentStates(this)
val followSetsFlow =
account
@@ -215,13 +199,6 @@ class AccountViewModel(
emptyList(),
)
val dmRelays: StateFlow<ChatMessageRelayListEvent?> = observeByAuthor(ChatMessageRelayListEvent.KIND, account.signer.pubKey)
val searchRelays: StateFlow<SearchRelayListEvent?> = observeByAuthor(SearchRelayListEvent.KIND, account.signer.pubKey)
val toastManager = ToastManager()
val feedStates = AccountFeedContentStates(this)
@OptIn(ExperimentalCoroutinesApi::class)
val notificationHasNewItems =
combineTransform(

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -63,18 +63,18 @@ import com.vitorpamplona.amethyst.ui.feeds.FeedEmpty
import com.vitorpamplona.amethyst.ui.feeds.FeedError
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
import com.vitorpamplona.amethyst.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.ui.screen.NostrUserListFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.DisappearingScaffold
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -118,7 +118,7 @@ fun ListsScreen(
},
openItem = {
currentCoroutineScope.launch(Dispatchers.IO) {
val note = followSetsViewModel.getFollowSetAddressable(it, accountViewModel.account)
val note = followSetsViewModel.getFollowSetNote(it, accountViewModel.account)
if (note != null) {
val event = note.event as PeopleListEvent
println("Found list, with title: ${event.nameOrTitle()}")

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -21,8 +21,9 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.nip01Core.core.value
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip51Lists.PeopleListEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
@Stable
data class FollowSet(
@@ -33,7 +34,7 @@ data class FollowSet(
val profileList: Set<String>,
) : NostrList(listVisibility = visibility, content = profileList) {
companion object {
fun mapEventToSet(
suspend fun mapEventToSet(
event: PeopleListEvent,
signer: NostrSigner,
): FollowSet {
@@ -41,9 +42,12 @@ data class FollowSet(
val dTag = event.dTag()
val listTitle = event.nameOrTitle() ?: dTag
val listDescription = event.description() ?: ""
val publicFollows = event.filterTagList("p", null)
val privateFollows = mutableListOf<String>()
event.privateTaggedUsers(signer) { userList -> privateFollows.addAll(userList) }
val publicFollows = event.publicPeople().map { it.toTagArray() }.map { it.value() }
val privateFollows =
event
.privatePeople(signer)
?.map { it.toTagArray() }
?.map { it.value() } ?: emptyList()
return if (publicFollows.isEmpty() && privateFollows.isNotEmpty()) {
FollowSet(
identifierTag = address.toValue(),

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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
@@ -22,18 +22,22 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.lists
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.FollowSetFeedFilter
import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists
import com.vitorpamplona.ammolite.relays.BundledUpdate
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
import com.vitorpamplona.quartz.nip51Lists.peopleList.PeopleListEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
@@ -42,9 +46,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.util.UUID
// TODO: Investigate the addition of feed filters, for bookmark sets and general ones.
abstract class NostrListFeedViewModel(
open class NostrListFeedViewModel(
val dataSource: FeedFilter<FollowSet>,
) : ViewModel(),
InvalidatableContent {
@@ -57,15 +62,28 @@ abstract class NostrListFeedViewModel(
}
}
fun getFollowSetAddressable(
addressValue: String,
fun getFollowSetNote(
noteIdentifier: String,
account: Account,
): AddressableNote? {
checkNotInMainThread()
val potentialNote = LocalCache.getAddressableNoteIfExists(Address.parse(addressValue)!!)
val potentialNote = account.userProfile().followSetNotes.find { it.dTag() == noteIdentifier }
return potentialNote
}
fun followSetExistsWithName(
setName: String,
account: Account,
): Boolean {
checkNotInMainThread()
val potentialNote =
account
.userProfile()
.followSetNotes
.find { (it.event as PeopleListEvent).nameOrTitle() == setName }
return potentialNote != null
}
override val isRefreshing: MutableState<Boolean> = mutableStateOf(false)
private fun refreshSuspended() {
@@ -96,6 +114,111 @@ abstract class NostrListFeedViewModel(
}
}
fun addFollowSet(
setName: String,
setDescription: String?,
setType: ListVisibility,
account: Account,
) {
if (account.settings.isWriteable()) {
println("You are in read-only mode. Please login to make modifications.")
} else {
viewModelScope.launch(Dispatchers.IO) {
PeopleListEvent.createListWithDescription(
dTag = UUID.randomUUID().toString(),
title = setName,
description = setDescription,
isPrivate = setType == ListVisibility.Private,
signer = account.signer,
) {
account.sendMyPublicAndPrivateOutbox(it)
}
}
}
}
fun renameFollowSet(
newName: String,
followSet: FollowSet,
account: Account,
) {
if (!account.settings.isWriteable()) {
println("You are in read-only mode. Please login to make modifications.")
} else {
viewModelScope.launch(Dispatchers.IO) {
val setEvent = getFollowSetNote(followSet.identifierTag, account)?.event as PeopleListEvent
PeopleListEvent.modifyListName(
earlierVersion = setEvent,
newName = newName,
signer = account.signer,
) {
account.sendMyPublicAndPrivateOutbox(it)
}
}
}
}
fun deleteFollowSet(
followSet: FollowSet,
account: Account,
) {
if (!account.settings.isWriteable()) {
println("You are in read-only mode. Please login to make modifications.")
return
} else {
viewModelScope.launch(Dispatchers.IO) {
val followSetEvent = getFollowSetNote(followSet.identifierTag, account)?.event as PeopleListEvent
val deletionEvent = account.signer.sign(DeletionEvent.build(listOf(followSetEvent)))
account.sendMyPublicAndPrivateOutbox(deletionEvent)
}
}
}
fun addUserToSet(
userProfileHex: String,
followSet: FollowSet,
account: Account,
) {
if (!account.settings.isWriteable()) {
println("You are in read-only mode. Please login to make modifications.")
return
} else {
viewModelScope.launch(Dispatchers.IO) {
val followSetEvent = getFollowSetNote(followSet.identifierTag, account)?.event as PeopleListEvent
PeopleListEvent.addUser(
earlierVersion = followSetEvent,
pubKeyHex = userProfileHex,
isPrivate = followSet.visibility == ListVisibility.Private,
signer = account.signer,
) {
account.sendMyPublicAndPrivateOutbox(it)
}
}
}
}
fun removeUserFromSet(
userProfileHex: String,
followSet: FollowSet,
account: Account,
) {
if (!account.settings.isWriteable()) {
println("You are in read-only mode. Please login to make modifications.")
return
} else {
viewModelScope.launch(Dispatchers.IO) {
val followSetEvent = getFollowSetNote(followSet.identifierTag, account)?.event as PeopleListEvent
PeopleListEvent.removeUser(
earlierVersion = followSetEvent,
pubKeyHex = userProfileHex,
signer = account.signer,
) {
account.sendMyPublicAndPrivateOutbox(it)
}
}
}
}
private fun updateFeed(sets: ImmutableList<FollowSet>) {
if (sets.isNotEmpty()) {
_feedContent.update { FollowSetState.Loaded(sets) }
@@ -104,13 +227,14 @@ abstract class NostrListFeedViewModel(
}
}
private val bundler = BundledUpdate(1000, Dispatchers.IO)
private val bundler = BundledUpdate(2000, Dispatchers.IO)
override fun invalidateData(ignoreIfDoing: Boolean) {
// refresh()
bundler.invalidate(ignoreIfDoing) {
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
refreshSuspended()
}
}
@@ -135,4 +259,11 @@ abstract class NostrListFeedViewModel(
collectorJob?.cancel()
super.onCleared()
}
@Stable
class Factory(
val account: Account,
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = NostrListFeedViewModel(FollowSetFeedFilter(account)) as T
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2024 Vitor Pamplona
* 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

View File

@@ -39,6 +39,7 @@ import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.nip51Lists.replaceAll
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
import com.vitorpamplona.quartz.utils.TimeUtils
@@ -214,5 +215,138 @@ class PeopleListEvent(
initializer()
}
suspend fun createListWithDescription(
dTag: String,
title: String,
description: String? = null,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
if (description == null) {
val newList =
create(
name = title,
person = UserTag(pubKey = signer.pubKey),
isPrivate = isPrivate,
signer = signer,
dTag = dTag,
createdAt = createdAt,
)
onReady(newList)
} else {
if (isPrivate) {
val event =
build(
name = title,
privatePeople = listOf(UserTag(pubKey = signer.pubKey)),
signer = signer,
dTag = dTag,
createdAt = createdAt,
) {
addUnique(arrayOf("description", description))
}
val list = signer.sign(event)
onReady(list)
} else {
val event =
build(
name = title,
publicPeople = listOf(UserTag(pubKey = signer.pubKey)),
signer = signer,
dTag = dTag,
createdAt = createdAt,
) {
addUnique(arrayOf("description", description))
}
val list = signer.sign(event)
onReady(list)
}
}
}
suspend fun createListWithUser(
name: String,
pubKeyHex: String,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
val newList =
create(
name = name,
person = UserTag(pubKey = pubKeyHex),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
)
onReady(newList)
}
suspend fun addUser(
earlierVersion: PeopleListEvent,
pubKeyHex: String,
isPrivate: Boolean,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
val newList =
add(
earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex),
isPrivate = isPrivate,
signer = signer,
createdAt = createdAt,
)
onReady(newList)
}
suspend fun removeUser(
earlierVersion: PeopleListEvent,
pubKeyHex: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit,
) {
val updatedList =
remove(
earlierVersion = earlierVersion,
person = UserTag(pubKey = pubKeyHex),
signer = signer,
createdAt = createdAt,
)
onReady(updatedList)
}
suspend fun modifyListName(
earlierVersion: PeopleListEvent,
newName: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
onReady: (PeopleListEvent) -> Unit = {},
) {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val currentTitle = earlierVersion.tags.first { it[0] == NameTag.TAG_NAME || it[0] == TitleTag.TAG_NAME }
val newTitleTag =
if (currentTitle[0] == NameTag.TAG_NAME) {
NameTag.assemble(newName)
} else {
com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
.assemble(newName)
}
val modified =
resign(
publicTags = earlierVersion.tags.replaceAll(currentTitle, newTitleTag),
privateTags = privateTags.replaceAll(currentTitle, newTitleTag),
signer = signer,
createdAt = createdAt,
)
onReady(modified)
}
}
}