From 70964e680a72a8de61cb9f632eca6cf8b18cd9d0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 23 Nov 2023 10:46:56 -0500 Subject: [PATCH] Migrates to MuteList 10000 --- .../vitorpamplona/amethyst/model/Account.kt | 130 ++++++---- .../amethyst/model/LocalCache.kt | 5 +- .../service/NostrAccountDataSource.kt | 3 +- .../vitorpamplona/amethyst/ui/MainActivity.kt | 7 +- .../amethyst/ui/dal/DiscoverChatFeedFilter.kt | 4 +- .../ui/dal/DiscoverCommunityFeedFilter.kt | 4 +- .../amethyst/ui/dal/DiscoverLiveFeedFilter.kt | 4 +- .../ui/dal/HiddenAccountsFeedFilter.kt | 6 +- .../ui/dal/HomeConversationsFeedFilter.kt | 4 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 4 +- .../amethyst/ui/dal/NotificationFeedFilter.kt | 4 +- .../amethyst/ui/dal/VideoFeedFilter.kt | 7 +- .../amethyst/ui/navigation/AppTopBar.kt | 12 +- .../ui/screen/loggedIn/AccountViewModel.kt | 3 +- .../ui/screen/loggedIn/HiddenUsersScreen.kt | 7 +- .../ui/screen/loggedIn/NotificationScreen.kt | 2 +- app/src/main/res/values/strings.xml | 1 + .../quartz/events/EmojiPackSelectionEvent.kt | 3 + .../quartz/events/EventFactory.kt | 1 + .../quartz/events/GeneralListEvent.kt | 23 ++ .../quartz/events/MuteListEvent.kt | 244 ++++++++++++------ .../quartz/events/PeopleListEvent.kt | 26 +- 22 files changed, 340 insertions(+), 164 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index dc20ca702..3b7ffa580 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -41,6 +41,7 @@ import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent +import com.vitorpamplona.quartz.events.GeneralListEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent @@ -51,6 +52,7 @@ import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.MetadataEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NIP24Factory import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PollNoteEvent @@ -71,6 +73,7 @@ import com.vitorpamplona.quartz.signers.NostrSignerInternal import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet +import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -299,7 +302,7 @@ class Account( } private fun decryptLiveFollows(peopleListFollows: NoteState?, onReady: (LiveFollowLists) -> Unit) { - val listEvent = (peopleListFollows?.note?.event as? PeopleListEvent) + val listEvent = (peopleListFollows?.note?.event as? GeneralListEvent) listEvent?.privateTags(signer) { privateTagList -> onReady( LiveFollowLists( @@ -321,27 +324,37 @@ class Account( ) val flowHiddenUsers: StateFlow by lazy { - combineTransform(live.asFlow(), getBlockListNote().flow().metadata.stateFlow) { localLive, blockList -> + combineTransform( + live.asFlow(), + getBlockListNote().flow().metadata.stateFlow, + getMuteListNote().flow().metadata.stateFlow + ) { localLive, blockList, muteList -> checkNotInMainThread() - val result = withTimeoutOrNull(1000) { + val resultBlockList = withTimeoutOrNull(1000) { suspendCancellableCoroutine { continuation -> (blockList.note.event as? PeopleListEvent)?.publicAndPrivateUsersAndWords(signer) { continuation.resume(it) } } - } + } ?: PeopleListEvent.UsersAndWords() - result?.let { - emit( - LiveHiddenUsers( - hiddenUsers = it.users, - hiddenWords = it.words, - spammers = localLive.account.transientHiddenUsers, - showSensitiveContent = localLive.account.showSensitiveContent - ) + val resultMuteList = withTimeoutOrNull(1000) { + suspendCancellableCoroutine { continuation -> + (muteList.note.event as? MuteListEvent)?.publicAndPrivateUsersAndWords(signer) { + continuation.resume(it) + } + } + } ?: PeopleListEvent.UsersAndWords() + + emit( + LiveHiddenUsers( + hiddenUsers = (resultBlockList.users + resultMuteList.users).toPersistentSet(), + hiddenWords = (resultBlockList.words + resultMuteList.words).toPersistentSet(), + spammers = localLive.account.transientHiddenUsers, + showSensitiveContent = localLive.account.showSensitiveContent ) - } + ) }.stateIn( scope, SharingStarted.Eagerly, @@ -1425,38 +1438,45 @@ class Account( return LocalCache.getOrCreateAddressableNote(aTag) } + fun getMuteListNote(): AddressableNote { + val aTag = ATag( + MuteListEvent.kind, + userProfile().pubkeyHex, + "", + null + ) + return LocalCache.getOrCreateAddressableNote(aTag) + } + fun getBlockList(): PeopleListEvent? { return getBlockListNote().event as? PeopleListEvent } - fun hideWord(word: String) { - val blockList = getBlockList() + fun getMuteList(): MuteListEvent? { + return getMuteListNote().event as? MuteListEvent + } - if (blockList != null) { - PeopleListEvent.addWord( - earlierVersion = blockList, + fun hideWord(word: String) { + val muteList = getMuteList() + + if (muteList != null) { + MuteListEvent.addWord( + earlierVersion = muteList, word = word, isPrivate = true, signer = signer ) { Client.send(it) LocalCache.consume(it) - - live.invalidateData() - saveable.invalidateData() } } else { - PeopleListEvent.createListWithWord( - name = PeopleListEvent.blockList, + MuteListEvent.createListWithWord( word = word, isPrivate = true, signer = signer ) { Client.send(it) LocalCache.consume(it) - - live.invalidateData() - saveable.invalidateData() } } } @@ -1473,46 +1493,50 @@ class Account( ) { Client.send(it) LocalCache.consume(it) + } + } - live.invalidateData() - saveable.invalidateData() + val muteList = getMuteList() + + if (muteList != null) { + MuteListEvent.removeWord( + earlierVersion = muteList, + word = word, + isPrivate = true, + signer = signer + ) { + Client.send(it) + LocalCache.consume(it) } } } - suspend fun hideUser(pubkeyHex: String) { - val blockList = getBlockList() + fun hideUser(pubkeyHex: String) { + val muteList = getMuteList() - if (blockList != null) { - PeopleListEvent.addUser( - earlierVersion = blockList, + if (muteList != null) { + MuteListEvent.addUser( + earlierVersion = muteList, pubKeyHex = pubkeyHex, isPrivate = true, signer = signer ) { Client.send(it) LocalCache.consume(it) - - live.invalidateData() - saveable.invalidateData() } } else { - PeopleListEvent.createListWithUser( - name = PeopleListEvent.blockList, + MuteListEvent.createListWithUser( pubKeyHex = pubkeyHex, isPrivate = true, signer = signer ) { Client.send(it) LocalCache.consume(it) - - live.invalidateData() - saveable.invalidateData() } } } - suspend fun showUser(pubkeyHex: String) { + fun showUser(pubkeyHex: String) { val blockList = getBlockList() if (blockList != null) { @@ -1524,12 +1548,26 @@ class Account( ) { Client.send(it) LocalCache.consume(it) - - transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet() - live.invalidateData() - saveable.invalidateData() } } + + val muteList = getMuteList() + + if (muteList != null) { + MuteListEvent.removeUser( + earlierVersion = muteList, + pubKeyHex = pubkeyHex, + isPrivate = true, + signer = signer + ) { + Client.send(it) + LocalCache.consume(it) + } + } + + transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet() + live.invalidateData() + saveable.invalidateData() } fun changeDefaultZapType(zapType: LnZapEvent.ZapType) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 20a783453..35651aba7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -57,6 +57,7 @@ import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent import com.vitorpamplona.quartz.events.MetadataEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.NNSEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PinListEvent @@ -425,6 +426,7 @@ object LocalCache { } } + fun consume(event: MuteListEvent) { consumeBaseReplaceable(event) } fun consume(event: PeopleListEvent) { consumeBaseReplaceable(event) } private fun consume(event: AdvertisedRelayListEvent) { consumeBaseReplaceable(event) } private fun consume(event: CommunityDefinitionEvent, relay: Relay?) { consumeBaseReplaceable(event) } @@ -696,7 +698,7 @@ object LocalCache { val author = getOrCreateUser(event.pubKey) val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } note.loadEvent(event, author, repliesTo) @@ -1561,6 +1563,7 @@ object LocalCache { is LnZapPaymentResponseEvent -> consume(event) is LongTextNoteEvent -> consume(event, relay) is MetadataEvent -> consume(event) + is MuteListEvent -> consume(event) is NNSEvent -> comsume(event) is PrivateDmEvent -> consume(event, relay) is PinListEvent -> consume(event) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index ef2d700ac..b1adad63e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -23,6 +23,7 @@ import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapPaymentResponseEvent import com.vitorpamplona.quartz.events.MetadataEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.ReactionEvent @@ -87,7 +88,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { return TypedFilter( types = COMMON_FEED_TYPES, filter = JsonFilter( - kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind), + kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind, MuteListEvent.kind), authors = listOf(account.userProfile().pubkeyHex), limit = 100 ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 6ac11c808..e7a594e10 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -167,9 +167,10 @@ class MainActivity : AppCompatActivity() { override fun onStop() { super.onStop() - GlobalScope.launch(Dispatchers.Default) { - serviceManager.trimMemory() - } + // Graph doesn't completely clear. + // GlobalScope.launch(Dispatchers.Default) { + // serviceManager.trimMemory() + // } Log.d("Lifetime Event", "MainActivity.onStop") } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt index 3236c5350..d41ecce0b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.IsInPublicChatChannel +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -16,7 +17,8 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt index 0fbd8ef8f..85dd9e524 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt @@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -16,7 +17,8 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte } override fun showHiddenKey(): Boolean { - return account.defaultDiscoveryFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultDiscoveryFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + account.defaultDiscoveryFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt index 71c11871b..f19047cc5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt @@ -9,6 +9,7 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_ENDED import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -24,7 +25,8 @@ open class DiscoverLiveFeedFilter( } override fun showHiddenKey(): Boolean { - return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt index 06a7ead3e..7b9a0b9ab 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HiddenAccountsFeedFilter.kt @@ -14,9 +14,9 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { } override fun feed(): List { - return account.liveHiddenUsers.value?.hiddenUsers?.map { + return account.flowHiddenUsers.value.hiddenUsers.map { LocalCache.getOrCreateUser(it) - } ?: emptyList() + } } } @@ -30,7 +30,7 @@ class HiddenWordsFeedFilter(val account: Account) : FeedFilter() { } override fun feed(): List { - return account.liveHiddenUsers.value?.hiddenWords?.toList() ?: emptyList() + return account.flowHiddenUsers.value.hiddenWords.toList() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index 350c4224d..530f8ee59 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent @@ -18,7 +19,8 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt index 7a9c85e8e..c1f68fbe0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeNewThreadFeedFilter.kt @@ -10,6 +10,7 @@ import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.HighlightEvent import com.vitorpamplona.quartz.events.LongTextNoteEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.PollNoteEvent import com.vitorpamplona.quartz.events.RepostEvent @@ -23,7 +24,8 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() } override fun showHiddenKey(): Boolean { - return account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultHomeFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + account.defaultHomeFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt index 5031128c5..7079ade96 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/NotificationFeedFilter.kt @@ -14,6 +14,7 @@ import com.vitorpamplona.quartz.events.GenericRepostEvent import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.RepostEvent @@ -24,7 +25,8 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() } override fun showHiddenKey(): Boolean { - return account.defaultNotificationFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultNotificationFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + account.defaultNotificationFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index b3e5d3c28..ed553c0a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.quartz.events.FileHeaderEvent import com.vitorpamplona.quartz.events.FileStorageHeaderEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import com.vitorpamplona.quartz.utils.TimeUtils @@ -15,7 +16,8 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { } override fun showHiddenKey(): Boolean { - return account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + return account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + account.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) } override fun feed(): List { @@ -31,7 +33,8 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { private fun innerApplyFilter(collection: Collection): Set { val now = TimeUtils.now() val isGlobal = account.defaultStoriesFollowList.value == GLOBAL_FOLLOWS - val isHiddenList = account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) + val isHiddenList = account.defaultStoriesFollowList.value == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) || + account.defaultStoriesFollowList.value == MuteListEvent.blockListFor(account.userProfile().pubkeyHex) val followingKeySet = account.liveStoriesFollowLists.value?.users ?: emptySet() val followingTagSet = account.liveStoriesFollowLists.value?.hashtags ?: emptySet() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 877df2534..13815a8b8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -118,6 +118,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.ChatroomKey import com.vitorpamplona.quartz.events.ContactListEvent +import com.vitorpamplona.quartz.events.MuteListEvent import com.vitorpamplona.quartz.events.PeopleListEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -589,6 +590,11 @@ data class CodeName(val code: String, val name: Name, val type: CodeNameType) class FollowListViewModel(val account: Account) : ViewModel() { val kind3Follow = CodeName(KIND3_FOLLOWS, ResourceName(R.string.follow_list_kind3follows), CodeNameType.HARDCODED) val globalFollow = CodeName(GLOBAL_FOLLOWS, ResourceName(R.string.follow_list_global), CodeNameType.HARDCODED) + val muteListFollow = CodeName( + MuteListEvent.blockListFor(account.userProfile().pubkeyHex), + ResourceName(R.string.follow_list_mute_list), + CodeNameType.HARDCODED + ) private var _kind3GlobalPeopleRoutes = MutableStateFlow>(emptyList().toPersistentList()) val kind3GlobalPeopleRoutes = _kind3GlobalPeopleRoutes.asStateFlow() @@ -634,13 +640,13 @@ class FollowListViewModel(val account: Account) : ViewModel() { val routeList = (communities + hashtags + geotags).sortedBy { it.name.name() } - val kind3GlobalPeopleRouteList = listOf(listOf(kind3Follow, globalFollow), newFollowLists, routeList).flatten().toImmutableList() + val kind3GlobalPeopleRouteList = listOf(listOf(kind3Follow, globalFollow), newFollowLists, routeList, listOf(muteListFollow)).flatten().toImmutableList() if (!equalImmutableLists(_kind3GlobalPeopleRoutes.value, kind3GlobalPeopleRouteList)) { _kind3GlobalPeopleRoutes.emit(kind3GlobalPeopleRouteList) } - val kind3GlobalPeopleList = listOf(listOf(kind3Follow, globalFollow), newFollowLists).flatten().toImmutableList() + val kind3GlobalPeopleList = listOf(listOf(kind3Follow, globalFollow), newFollowLists, listOf(muteListFollow)).flatten().toImmutableList() if (!equalImmutableLists(_kind3GlobalPeople.value, kind3GlobalPeopleList)) { _kind3GlobalPeople.emit(kind3GlobalPeopleList) @@ -656,7 +662,7 @@ class FollowListViewModel(val account: Account) : ViewModel() { LocalCache.live.newEventBundles.collect { newNotes -> checkNotInMainThread() if (newNotes.any { - it.event?.pubKey() == account.userProfile().pubkeyHex && (it.event is PeopleListEvent || it.event is ContactListEvent) + it.event?.pubKey() == account.userProfile().pubkeyHex && (it.event is PeopleListEvent || it.event is MuteListEvent || it.event is ContactListEvent) } ) { refresh() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 93be5ec7b..c295d30fe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -153,8 +153,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View } fun isNoteHidden(note: Note): Boolean { - val isSensitive = note.event?.isSensitive() ?: false - return account.isHidden(note.author!!) || (isSensitive && account.showSensitiveContent == false) + return note.isHiddenFor(account.flowHiddenUsers.value) } fun hasReactedTo(baseNote: Note, reaction: String): Boolean { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 0c6342de6..c40e26c88 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel @@ -214,7 +215,9 @@ private fun AddMuteWordTextField(accountViewModel: AccountViewModel) { value = currentWordToAdd.value, onValueChange = { currentWordToAdd.value = it }, label = { Text(text = stringResource(R.string.hide_new_word_label)) }, - modifier = Modifier.fillMaxWidth().padding(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), placeholder = { Text( text = stringResource(R.string.hide_new_word_label), @@ -260,7 +263,7 @@ fun WatchAccountAndBlockList( accountViewModel: AccountViewModel ) { val accountState by accountViewModel.accountLiveData.observeAsState() - val blockListState by accountViewModel.account.getBlockListNote().live().metadata.observeAsState() + val blockListState by accountViewModel.account.flowHiddenUsers.collectAsStateWithLifecycle() LaunchedEffect(accountViewModel, accountState, blockListState) { feedViewModel.invalidateData() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index c4624a814..abc5ea360 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -142,7 +142,7 @@ fun WatchAccountForNotifications( notifFeedViewModel: NotificationViewModel, accountViewModel: AccountViewModel ) { - val listState by accountViewModel.account.liveStoriesFollowLists.collectAsStateWithLifecycle() + val listState by accountViewModel.account.liveNotificationFollowLists.collectAsStateWithLifecycle() LaunchedEffect(accountViewModel, listState) { NostrAccountDataSource.account = accountViewModel.account diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3901f92ca..6e373593b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -386,6 +386,7 @@ Follow List All Follows Global + Mute List ## Connect through Tor with Orbot \n\n1. Install [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt index 1ee67269f..9925800b3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EmojiPackSelectionEvent.kt @@ -18,8 +18,11 @@ class EmojiPackSelectionEvent( content: String, sig: HexKey ) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { + override fun dTag() = fixedDTag + companion object { const val kind = 10030 + const val fixedDTag = "" fun create( listOfEmojiPacks: List?, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt index c18ad4b67..6b6319d8d 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/EventFactory.kt @@ -66,6 +66,7 @@ class EventFactory { LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig) MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig) + MuteListEvent.kind -> MuteListEvent(id, pubKey, createdAt, tags, content, sig) NNSEvent.kind -> NNSEvent(id, pubKey, createdAt, tags, content, sig) PeopleListEvent.kind -> PeopleListEvent(id, pubKey, createdAt, tags, content, sig) PinListEvent.kind -> PinListEvent(id, pubKey, createdAt, tags, content, sig) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index a80c8628d..c96ff1656 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -8,6 +8,8 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.toImmutableSet @Immutable abstract class GeneralListEvent( @@ -34,6 +36,27 @@ abstract class GeneralListEvent( return privateTagsCache } + fun filterTagList(key: String, privateTags: List>?): ImmutableSet { + val privateUserList = privateTags?.let { + it.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet() + } ?: emptySet() + val publicUserList = tags.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet() + + return (privateUserList + publicUserList).toImmutableSet() + } + + fun isTagged(key: String, tag: String, isPrivate: Boolean, signer: NostrSigner, onReady: (Boolean) -> Unit) { + return if (isPrivate) { + privateTagsOrEmpty(signer = signer) { + onReady( + it.any { it.size > 1 && it[0] == key && it[1] == tag } + ) + } + } else { + onReady(isTagged(key, tag)) + } + } + fun privateTags(signer: NostrSigner, onReady: (List>) -> Unit) { if (content.isBlank()) return diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt index 607a6e2f5..626d8f212 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/MuteListEvent.kt @@ -10,6 +10,7 @@ import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner +import kotlinx.collections.immutable.ImmutableSet import java.util.UUID @Immutable @@ -20,96 +21,195 @@ class MuteListEvent( tags: List>, content: String, sig: HexKey -) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { +) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { @Transient - private var privateTagsCache: List>? = null + var publicAndPrivateUserCache: ImmutableSet? = null + @Transient + var publicAndPrivateWordCache: ImmutableSet? = null - private fun privateTags(signer: NostrSigner, onReady: (List>) -> Unit) { - if (content.isBlank()) return + override fun dTag() = fixedDTag - privateTagsCache?.let { - onReady(it) - return + fun publicAndPrivateUsersAndWords(signer: NostrSigner, onReady: (PeopleListEvent.UsersAndWords) -> Unit) { + publicAndPrivateUserCache?.let { userList -> + publicAndPrivateWordCache?.let { wordList -> + onReady(PeopleListEvent.UsersAndWords(userList, wordList)) + return + } } - try { - signer.nip04Decrypt(content, pubKey) { - privateTagsCache = mapper.readValue>>(it) - privateTagsCache?.let { - onReady(it) + privateTagsOrEmpty(signer) { + publicAndPrivateUserCache = filterTagList("p", it) + publicAndPrivateWordCache = filterTagList("word", it) + + publicAndPrivateUserCache?.let { userList -> + publicAndPrivateWordCache?.let { wordList -> + onReady( + PeopleListEvent.UsersAndWords(userList, wordList) + ) } } - } catch (e: Throwable) { - Log.w("MuteList", "Error parsing the JSON ${e.message}") } } - fun privateTaggedUsers(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { - onReady(it.filter { it.size > 1 && it[0] == "p" }.map { it[1] } ) - } - fun privateHashtags(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { - onReady(it.filter { it.size > 1 && it[0] == "t" }.map { it[1] } ) - } - fun privateGeohashes(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { - onReady(it.filter { it.size > 1 && it[0] == "g" }.map { it[1] } ) - } - fun privateTaggedEvents(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { - onReady(it.filter { it.size > 1 && it[0] == "e" }.map { it[1] } ) - } - - fun privateTaggedAddresses(signer: NostrSigner, onReady: (List) -> Unit) = privateTags(signer) { - onReady( - it.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - ) - } - companion object { const val kind = 10000 + const val fixedDTag = "" + + fun blockListFor(pubKeyHex: HexKey): String { + return "10000:$pubKeyHex:" + } + + fun createListWithTag(key: String, tag: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + if (isPrivate) { + encryptTags(listOf(listOf(key, tag)), signer) { encryptedTags -> + create( + content = encryptedTags, + tags = emptyList(), + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } else { + create( + content = "", + tags = listOf(listOf(key, tag)), + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + + fun createListWithUser(pubKeyHex: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + return createListWithTag("p", pubKeyHex, isPrivate, signer, createdAt, onReady) + } + + fun createListWithWord(word: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + return createListWithTag("word", word, isPrivate, signer, createdAt, onReady) + } + + fun addUsers(earlierVersion: MuteListEvent, listPubKeyHex: List, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = privateTags.plus( + listPubKeyHex.map { + listOf("p", it) + } + ), + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus( + listPubKeyHex.map { + listOf("p", it) + } + ), + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + + fun addWord(earlierVersion: MuteListEvent, word: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + return addTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) + } + + fun addUser(earlierVersion: MuteListEvent, pubKeyHex: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + return addTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) + } + + fun addTag(earlierVersion: MuteListEvent, key: String, tag: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> + if (!isTagged) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = privateTags.plus(element = listOf(key, tag)), + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = listOf(key, tag)), + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } + } + + fun removeWord(earlierVersion: MuteListEvent, word: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + return removeTag(earlierVersion, "word", word, isPrivate, signer, createdAt, onReady) + } + + fun removeUser(earlierVersion: MuteListEvent, pubKeyHex: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, signer, createdAt, onReady) + } + + fun removeTag(earlierVersion: MuteListEvent, key: String, tag: String, isPrivate: Boolean, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit) { + earlierVersion.isTagged(key, tag, isPrivate, signer) { isTagged -> + if (isTagged) { + if (isPrivate) { + earlierVersion.privateTagsOrEmpty(signer) { privateTags -> + encryptTags( + privateTags = privateTags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }, + signer = signer + ) { encryptedTags -> + create( + content = encryptedTags, + tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) }, + signer = signer, + createdAt = createdAt, + onReady = onReady + ) + } + } + } + } fun create( - events: List? = null, - users: List? = null, - addresses: List? = null, - - privEvents: List? = null, - privUsers: List? = null, - privAddresses: List? = null, - + content: String, + tags: List>, signer: NostrSigner, createdAt: Long = TimeUtils.now(), onReady: (MuteListEvent) -> Unit ) { - val privTags = mutableListOf>() - privEvents?.forEach { - privTags.add(listOf("e", it)) - } - privUsers?.forEach { - privTags.add(listOf("p", it)) - } - privAddresses?.forEach { - privTags.add(listOf("a", it.toTag())) - } - val msg = mapper.writeValueAsString(privTags) - - val tags = mutableListOf>() - events?.forEach { - tags.add(listOf("e", it)) - } - users?.forEach { - tags.add(listOf("p", it)) - } - addresses?.forEach { - tags.add(listOf("a", it.toTag())) - } - - signer.nip04Encrypt(msg, signer.pubKey) { content -> - signer.sign(createdAt, kind, tags, content, onReady) - } + signer.sign(createdAt, kind, tags, content, onReady) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt index ca17519ef..6f2cd52e3 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PeopleListEvent.kt @@ -24,15 +24,6 @@ class PeopleListEvent( @Transient var publicAndPrivateWordCache: ImmutableSet? = null - fun filterTagList(key: String, privateTags: List>?): ImmutableSet { - val privateUserList = privateTags?.let { - it.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet() - } ?: emptySet() - val publicUserList = tags.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet() - - return (privateUserList + publicUserList).toImmutableSet() - } - fun publicAndPrivateWords(signer: NostrSigner, onReady: (ImmutableSet) -> Unit) { publicAndPrivateWordCache?.let { onReady(it) @@ -62,7 +53,10 @@ class PeopleListEvent( } @Immutable - data class UsersAndWords(val users: ImmutableSet, val words: ImmutableSet) + data class UsersAndWords( + val users: ImmutableSet = persistentSetOf(), + val words: ImmutableSet = persistentSetOf() + ) fun publicAndPrivateUsersAndWords(signer: NostrSigner, onReady: (UsersAndWords) -> Unit) { publicAndPrivateUserCache?.let { userList -> @@ -86,18 +80,6 @@ class PeopleListEvent( } } - fun isTagged(key: String, tag: String, isPrivate: Boolean, signer: NostrSigner, onReady: (Boolean) -> Unit) { - return if (isPrivate) { - privateTagsOrEmpty(signer = signer) { - onReady( - it.any { it.size > 1 && it[0] == key && it[1] == tag } - ) - } - } else { - onReady(isTagged(key, tag)) - } - } - fun isTaggedWord(word: String, isPrivate: Boolean, signer: NostrSigner, onReady: (Boolean) -> Unit) = isTagged( "word", word, isPrivate, signer, onReady) fun isTaggedUser(idHex: String, isPrivate: Boolean, signer: NostrSigner, onReady: (Boolean) -> Unit) = isTagged( "p", idHex, isPrivate, signer, onReady)