mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-10 19:16:47 +01:00
Add other functions and adapt them to the code already present.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()}")
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user