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 6171f8f72..c28e9fe10 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map import com.vitorpamplona.amethyst.OptOutFromFilters import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource @@ -18,6 +20,10 @@ import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.actions.ServersAvailable import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.note.Nip47URI +import com.vitorpamplona.amethyst.ui.note.combineWith +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableSet import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -49,9 +55,11 @@ val KIND3_FOLLOWS = " All Follows " @Stable class Account( val loggedIn: Persona, - var followingChannels: Set = DefaultChannels, - var followingCommunities: Set = setOf(), - var hiddenUsers: Set = setOf(), + + var followingChannels: Set = DefaultChannels, // deprecated + var followingCommunities: Set = setOf(), // deprecated + var hiddenUsers: Set = setOf(), // deprecated + var localRelays: Set = Constants.defaultRelays.toSet(), var dontTranslateFrom: Set = getLanguagesSpokenByUser(), var languagePreferences: Map = mapOf(), @@ -76,7 +84,7 @@ class Account( var lastReadPerRoute: Map = mapOf(), var settings: Settings = Settings() ) { - var transientHiddenUsers: Set = setOf() + var transientHiddenUsers: ImmutableSet = persistentSetOf() // Observers line up here. val live: AccountLiveData = AccountLiveData(this) @@ -84,7 +92,24 @@ class Account( val liveLastRead: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this) + @Immutable + data class LiveHiddenUsers( + val hiddenUsers: ImmutableSet, + val spammers: ImmutableSet, + val showSensitiveContent: Boolean? + ) + + val liveHiddenUsers: LiveData = live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent -> + val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(loggedIn.privKey) + LiveHiddenUsers( + hiddenUsers = liveBlockedUsers ?: persistentSetOf(), + spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), + showSensitiveContent = showSensitiveContent + ) + }.distinctUntilChanged() + var userProfileCache: User? = null + fun updateAutomaticallyStartPlayback( automaticallyStartPlayback: Boolean? ) { @@ -114,7 +139,7 @@ class Account( filterSpamFromStrangers = filterSpam OptOutFromFilters.start(warnAboutPostsWithReports, filterSpamFromStrangers) if (!filterSpamFromStrangers) { - transientHiddenUsers = setOf() + transientHiddenUsers = persistentSetOf() } live.invalidateData() saveable.invalidateData() @@ -128,14 +153,6 @@ class Account( } } - fun followingChannels(): List { - return followingChannels.mapNotNull { LocalCache.checkGetOrCreateChannel(it) } - } - - fun followingCommunities(): List { - return followingCommunities.mapNotNull { LocalCache.checkGetOrCreateAddressableNote(it) } - } - fun isWriteable(): Boolean { return loggedIn.privKey != null } @@ -144,21 +161,25 @@ class Account( if (!isWriteable()) return val contactList = userProfile().latestContactList - val follows = contactList?.follows() ?: emptyList() - val followsTags = contactList?.unverifiedFollowTagSet() ?: emptyList() - if (contactList != null && follows.isNotEmpty()) { - val event = ContactListEvent.create( - follows, - followsTags, - relays, - loggedIn.privKey!! + if (contactList != null && contactList.tags.isNotEmpty()) { + val event = ContactListEvent.updateRelayList( + earlierVersion = contactList, + relayUse = relays, + privateKey = loggedIn.privKey!! ) Client.send(event) LocalCache.consume(event) } else { - val event = ContactListEvent.create(listOf(), listOf(), relays, loggedIn.privKey!!) + val event = ContactListEvent.createFromScratch( + followUsers = listOf(), + followTags = listOf(), + followCommunities = listOf(), + followEvents = DefaultChannels.toList(), + relayUse = relays, + privateKey = loggedIn.privKey!! + ) // Keep this local to avoid erasing a good contact list. // Client.send(event) @@ -398,27 +419,90 @@ class Account( } } + private fun migrateCommunitiesAndChannelsIfNeeded(latestContactList: ContactListEvent?): ContactListEvent? { + if (latestContactList == null) return latestContactList + + var returningContactList: ContactListEvent = latestContactList + + if (followingCommunities.isNotEmpty()) { + followingCommunities.forEach { + ATag.parse(it, null)?.let { + returningContactList = ContactListEvent.followAddressableEvent(returningContactList, it, loggedIn.privKey!!) + } + } + followingCommunities = emptySet() + } + + if (followingChannels.isNotEmpty()) { + followingChannels.forEach { + returningContactList = ContactListEvent.followEvent(returningContactList, it, loggedIn.privKey!!) + } + followingChannels = emptySet() + } + + return returningContactList + } + fun follow(user: User) { if (!isWriteable()) return - val contactList = userProfile().latestContactList - val followingUsers = contactList?.follows() ?: emptyList() - val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) val event = if (contactList != null) { - ContactListEvent.create( - followingUsers.plus(Contact(user.pubkeyHex, null)), - followingTags, - contactList.relays(), - loggedIn.privKey!! + ContactListEvent.followUser(contactList, user.pubkeyHex, loggedIn.privKey!!) + } else { + ContactListEvent.createFromScratch( + followUsers = listOf(Contact(user.pubkeyHex, null)), + followTags = emptyList(), + followCommunities = emptyList(), + followEvents = DefaultChannels.toList(), + relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, + privateKey = loggedIn.privKey!! ) + } + + Client.send(event) + LocalCache.consume(event) + } + + fun follow(channel: Channel) { + if (!isWriteable()) return + + val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + + val event = if (contactList != null) { + ContactListEvent.followEvent(contactList, channel.idHex, loggedIn.privKey!!) + } else { + ContactListEvent.createFromScratch( + followUsers = emptyList(), + followTags = emptyList(), + followCommunities = emptyList(), + followEvents = DefaultChannels.toList().plus(channel.idHex), + relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, + privateKey = loggedIn.privKey!! + ) + } + + Client.send(event) + LocalCache.consume(event) + } + + fun follow(community: AddressableNote) { + if (!isWriteable()) return + + val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + + val event = if (contactList != null) { + ContactListEvent.followAddressableEvent(contactList, community.address, loggedIn.privKey!!) } else { val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } - ContactListEvent.create( - listOf(Contact(user.pubkeyHex, null)), - followingTags, - relays, - loggedIn.privKey!! + ContactListEvent.createFromScratch( + followUsers = emptyList(), + followTags = emptyList(), + followCommunities = listOf(community.address), + followEvents = DefaultChannels.toList(), + relayUse = relays, + privateKey = loggedIn.privKey!! ) } @@ -429,24 +513,22 @@ class Account( fun follow(tag: String) { if (!isWriteable()) return - val contactList = userProfile().latestContactList - val followingUsers = contactList?.follows() ?: emptyList() - val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) val event = if (contactList != null) { - ContactListEvent.create( - followingUsers, - followingTags.plus(tag), - contactList.relays(), + ContactListEvent.followHashtag( + contactList, + tag, loggedIn.privKey!! ) } else { - val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } - ContactListEvent.create( - followingUsers, - followingTags.plus(tag), - relays, - loggedIn.privKey!! + ContactListEvent.createFromScratch( + followUsers = emptyList(), + followTags = listOf(tag), + followCommunities = emptyList(), + followEvents = DefaultChannels.toList(), + relayUse = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }, + privateKey = loggedIn.privKey!! ) } @@ -457,15 +539,12 @@ class Account( fun unfollow(user: User) { if (!isWriteable()) return - val contactList = userProfile().latestContactList - val followingUsers = contactList?.follows() ?: emptyList() - val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) - if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) { - val event = ContactListEvent.create( - followingUsers.filter { it.pubKeyHex != user.pubkeyHex }, - followingTags, - contactList.relays(), + if (contactList != null && contactList.tags.isNotEmpty()) { + val event = ContactListEvent.unfollowUser( + contactList, + user.pubkeyHex, loggedIn.privKey!! ) @@ -477,15 +556,46 @@ class Account( fun unfollow(tag: String) { if (!isWriteable()) return - val contactList = userProfile().latestContactList - val followingUsers = contactList?.follows() ?: emptyList() - val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList() + val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) - if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) { - val event = ContactListEvent.create( - followingUsers, - followingTags.filter { !it.equals(tag, ignoreCase = true) }, - contactList.relays(), + if (contactList != null && contactList.tags.isNotEmpty()) { + val event = ContactListEvent.unfollowHashtag( + contactList, + tag, + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + } + + fun unfollow(channel: Channel) { + if (!isWriteable()) return + + val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + + if (contactList != null && contactList.tags.isNotEmpty()) { + val event = ContactListEvent.unfollowEvent( + contactList, + channel.idHex, + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + } + + fun unfollow(community: AddressableNote) { + if (!isWriteable()) return + + val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList) + + if (contactList != null && contactList.tags.isNotEmpty()) { + val event = ContactListEvent.unfollowAddressableEvent( + contactList, + community.address, loggedIn.privKey!! ) @@ -708,7 +818,9 @@ class Account( Client.send(event) LocalCache.consume(event) - joinChannel(event.id) + LocalCache.getChannelIfExists(event.id)?.let { + follow(it) + } } fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) { @@ -932,43 +1044,70 @@ class Account( } } - fun joinChannel(idHex: String) { - followingChannels = followingChannels + idHex - live.invalidateData() - - saveable.invalidateData() + fun getBlockListNote(): AddressableNote { + val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, PeopleListEvent.blockList, null) + return LocalCache.getOrCreateAddressableNote(aTag) } - fun leaveChannel(idHex: String) { - followingChannels = followingChannels - idHex - live.invalidateData() - - saveable.invalidateData() + fun getBlockList(): PeopleListEvent? { + return getBlockListNote().event as? PeopleListEvent } - fun joinCommunity(idHex: String) { - followingCommunities = followingCommunities + idHex - live.invalidateData() + private fun migrateHiddenUsersIfNeeded(latestList: PeopleListEvent?): PeopleListEvent? { + if (latestList == null) return latestList - saveable.invalidateData() - } + var returningList: PeopleListEvent = latestList - fun leaveCommunity(idHex: String) { - followingCommunities = followingCommunities - idHex - live.invalidateData() + if (hiddenUsers.isNotEmpty()) { + returningList = PeopleListEvent.addUsers(returningList, hiddenUsers.toList(), true, loggedIn.privKey!!) + hiddenUsers = emptySet() + } - saveable.invalidateData() + return returningList } fun hideUser(pubkeyHex: String) { - hiddenUsers = hiddenUsers + pubkeyHex + val blockList = migrateHiddenUsersIfNeeded(getBlockList()) + + val event = if (blockList != null) { + PeopleListEvent.addUser( + earlierVersion = blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + privateKey = loggedIn.privKey!! + ) + } else { + PeopleListEvent.createListWithUser( + name = PeopleListEvent.blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + privateKey = loggedIn.privKey!! + ) + } + + Client.send(event) + LocalCache.consume(event) + live.invalidateData() saveable.invalidateData() } fun showUser(pubkeyHex: String) { - hiddenUsers = hiddenUsers - pubkeyHex - transientHiddenUsers = transientHiddenUsers - pubkeyHex + val blockList = migrateHiddenUsersIfNeeded(getBlockList()) + + if (blockList != null) { + val event = PeopleListEvent.removeUser( + earlierVersion = blockList, + pubKeyHex = pubkeyHex, + isPrivate = true, + privateKey = loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + transientHiddenUsers = (transientHiddenUsers - pubkeyHex).toImmutableSet() live.invalidateData() saveable.invalidateData() } @@ -1055,7 +1194,53 @@ class Account( if (listName == GLOBAL_FOLLOWS) return null if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingTagSet() - return emptySet() + val privKey = loggedIn.privKey + + return if (listName != null) { + val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag() + val list = LocalCache.addressables[aTag] + if (list != null) { + val publicAddresses = list.event?.hashtags() ?: emptySet() + val privateAddresses = privKey?.let { + (list.event as? GeneralListEvent)?.privateHashtags(it) + } ?: emptySet() + + (publicAddresses + privateAddresses).toSet() + } else { + emptySet() + } + } else { + emptySet() + } + } + + fun selectedCommunitiesFollowList(listName: String?): Set? { + if (listName == GLOBAL_FOLLOWS) return null + if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingCommunitiesSet() + + val privKey = loggedIn.privKey + + return if (listName != null) { + val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, listName, null).toTag() + val list = LocalCache.addressables[aTag] + if (list != null) { + val publicAddresses = list.event?.taggedAddresses()?.map { it.toTag() } ?: emptySet() + val privateAddresses = privKey?.let { + (list.event as? GeneralListEvent)?.privateTaggedAddresses(it)?.map { it.toTag() } + } ?: emptySet() + + (publicAddresses + privateAddresses).toSet() + } else { + emptySet() + } + } else { + emptySet() + } + } + + fun selectedChatsFollowList(): Set { + val contactList = userProfile().latestContactList + return contactList?.taggedEvents()?.toSet() ?: DefaultChannels } fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) { @@ -1076,7 +1261,7 @@ class Account( Client.send(event) LocalCache.consume(event) - joinChannel(event.id) + follow(channel) } fun decryptContent(note: Note): String? { @@ -1224,7 +1409,11 @@ class Account( } fun isHidden(user: User) = isHidden(user.pubkeyHex) - fun isHidden(userHex: String) = userHex in hiddenUsers || userHex in transientHiddenUsers + fun isHidden(userHex: String): Boolean { + val blockList = getBlockList() + + return (blockList?.publicAndPrivateUsers(loggedIn.privKey)?.contains(userHex) ?: false) || userHex in transientHiddenUsers + } fun followingKeySet(): Set { return userProfile().cachedFollowingKeySet() @@ -1343,7 +1532,7 @@ class Account( it.cache.spamMessages.snapshot().values.forEach { if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) { - transientHiddenUsers = transientHiddenUsers + it.pubkeyHex + transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex).toImmutableSet() live.invalidateData() } } @@ -1356,6 +1545,7 @@ class Account( Log.d("Init", "Account") backupContactList?.let { println("Loading saved contacts ${it.toJson()}") + if (userProfile().latestContactList == null) { LocalCache.consume(it) } 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 3bf4714a8..39e197154 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -33,7 +33,7 @@ object LocalCache { ConcurrentHashMap Unit>>(10) fun checkGetOrCreateUser(key: String): User? { - checkNotInMainThread() + // checkNotInMainThread() if (isValidHex(key)) { return getOrCreateUser(key) @@ -143,7 +143,7 @@ object LocalCache { } fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote { - checkNotInMainThread() + // checkNotInMainThread() // we can't use naddr here because naddr might include relay info and // the preferred relay should not be part of the index. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 33d4d2734..f060b8152 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -563,6 +563,15 @@ open class Note(val idHex: String) { liveSet = null } } + + fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean { + if (event == null) return false + + val isSensitive = event?.isSensitive() ?: false + return accountChoices.hiddenUsers.contains(author?.pubkeyHex) || + accountChoices.spammers.contains(author?.pubkeyHex) || + (isSensitive && accountChoices.showSensitiveContent == false) + } } class NoteLiveSet(u: Note) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 3164561d4..2f083c7af 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -275,6 +275,10 @@ class User(val pubkeyHex: String) { return latestContactList?.verifiedFollowTagSet ?: emptySet() } + fun cachedFollowingCommunitiesSet(): Set { + return latestContactList?.verifiedFollowCommunitySet ?: emptySet() + } + fun cachedFollowCount(): Int? { return latestContactList?.verifiedFollowKeySet?.size } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index 7ea05591b..5e0f92f44 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -44,17 +44,23 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") { ) ) - fun createMyChannelsFilter() = TypedFilter( - types = COMMON_FEED_TYPES, // Metadata comes from any relay - filter = JsonFilter( - kinds = listOf(ChannelCreateEvent.kind), - ids = account.followingChannels.toList(), - since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList - ) - ) + fun createMyChannelsFilter(): TypedFilter { + val followingEvents = account.selectedChatsFollowList() - fun createLastChannelInfoFilter(): List { - return account.followingChannels.map { + return TypedFilter( + types = COMMON_FEED_TYPES, // Metadata comes from any relay + filter = JsonFilter( + kinds = listOf(ChannelCreateEvent.kind), + ids = followingEvents.toList(), + since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList + ) + ) + } + + fun createLastChannelInfoFilter(): List? { + val followingEvents = account.selectedChatsFollowList() + + return followingEvents.map { TypedFilter( types = COMMON_FEED_TYPES, // Metadata comes from any relay filter = JsonFilter( @@ -66,8 +72,10 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") { } } - fun createLastMessageOfEachChannelFilter(): List { - return account.followingChannels.map { + fun createLastMessageOfEachChannelFilter(): List? { + val followingEvents = account.selectedChatsFollowList() + + return followingEvents.map { TypedFilter( types = setOf(FeedType.PUBLIC_CHATS), filter = JsonFilter( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt index 362de96d5..53da14380 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrHomeDataSource.kt @@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent +import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent import com.vitorpamplona.amethyst.service.model.GenericRepostEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent @@ -103,11 +104,37 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") { ) } + fun createFollowCommunitiesFilter(): TypedFilter? { + val communitiesToLoad = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet() + + if (communitiesToLoad.isEmpty()) return null + + return TypedFilter( + types = setOf(FeedType.FOLLOWS), + filter = JsonFilter( + kinds = listOf( + TextNoteEvent.kind, + LongTextNoteEvent.kind, + ClassifiedsEvent.kind, + HighlightEvent.kind, + AudioTrackEvent.kind, + PinListEvent.kind, + CommunityPostApprovalEvent.kind + ), + tags = mapOf( + "a" to communitiesToLoad.toList() + ), + limit = 100, + since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList + ) + ) + } + val followAccountChannel = requestNewChannel { time, relayUrl -> latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList, relayUrl, time) } override fun updateChannelFilters() { - followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowTagsFilter()).ifEmpty { null } + followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowCommunitiesFilter(), createFollowTagsFilter()).ifEmpty { null } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt index 5a8ad5d70..361aca741 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt @@ -39,6 +39,10 @@ class ContactListEvent( unverifiedFollowTagSet().map { it.lowercase() }.toSet() } + val verifiedFollowCommunitySet: Set by lazy { + unverifiedFollowAddressSet().toSet() + } + val verifiedFollowKeySetAndMe: Set by lazy { verifiedFollowKeySet + pubKey } @@ -46,6 +50,8 @@ class ContactListEvent( fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) } fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.mapNotNull { it.getOrNull(1) } + fun unverifiedFollowAddressSet() = tags.filter { it[0] == "a" }.mapNotNull { it.getOrNull(1) } + fun follows() = tags.filter { it[0] == "p" }.mapNotNull { try { Contact(decodePublicKey(it[1]).toHexKey(), it.getOrNull(2)) @@ -73,14 +79,22 @@ class ContactListEvent( companion object { const val kind = 3 - fun create(follows: List, followTags: List, relayUse: Map?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + fun createFromScratch( + followUsers: List, + followTags: List, + followCommunities: List, + followEvents: List, + relayUse: Map?, + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): ContactListEvent { val content = if (relayUse != null) { gson.toJson(relayUse) } else { "" } - val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - val tags = follows.map { + + val tags = followUsers.map { if (it.relayUri != null) { listOf("p", it.pubKeyHex, it.relayUri) } else { @@ -88,8 +102,129 @@ class ContactListEvent( } } + followTags.map { listOf("t", it) + } + followEvents.map { + listOf("e", it) + } + followCommunities.map { + if (it.relay != null) { + listOf("a", it.toTag(), it.relay) + } else { + listOf("a", it.toTag()) + } } + return create( + content = content, + tags = tags, + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun followUser(earlierVersion: ContactListEvent, pubKeyHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + if (earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion + + return create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)), + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun unfollowUser(earlierVersion: ContactListEvent, pubKeyHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + if (!earlierVersion.isTaggedUser(pubKeyHex)) return earlierVersion + + return create( + content = earlierVersion.content, + tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex }, + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun followHashtag(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + if (earlierVersion.isTaggedHash(hashtag)) return earlierVersion + + return create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = listOf("t", hashtag)), + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun unfollowHashtag(earlierVersion: ContactListEvent, hashtag: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + if (!earlierVersion.isTaggedHash(hashtag)) return earlierVersion + + return create( + content = earlierVersion.content, + tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag }, + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun followEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + if (earlierVersion.isTaggedEvent(idHex)) return earlierVersion + + return create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = listOf("e", idHex)), + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun unfollowEvent(earlierVersion: ContactListEvent, idHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + if (!earlierVersion.isTaggedEvent(idHex)) return earlierVersion + + return create( + content = earlierVersion.content, + tags = earlierVersion.tags.filter { it.size > 1 && it[1] != idHex }, + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun followAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + if (earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion + + return create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = listOfNotNull("a", aTag.toTag(), aTag.relay)), + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun unfollowAddressableEvent(earlierVersion: ContactListEvent, aTag: ATag, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + if (!earlierVersion.isTaggedAddressableNote(aTag.toTag())) return earlierVersion + + return create( + content = earlierVersion.content, + tags = earlierVersion.tags.filter { it.size > 1 && it[1] != aTag.toTag() }, + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun updateRelayList(earlierVersion: ContactListEvent, relayUse: Map?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + val content = if (relayUse != null) { + gson.toJson(relayUse) + } else { + "" + } + + return create( + content = content, + tags = earlierVersion.tags, + privateKey = privateKey, + createdAt = createdAt + ) + } + + fun create(content: String, tags: List>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 56ac35430..ed3554f34 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -44,10 +44,10 @@ open class Event( fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" } - fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } - fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } + override fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } + override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] } - fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } + override fun taggedUrls() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] } override fun taggedEmojis() = tags.filter { it.size > 2 && it[0] == "emoji" }.map { EmojiUrl(it[1], it[2]) } @@ -63,7 +63,7 @@ open class Event( override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1) - fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { + override fun taggedAddresses() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull { val aTagValue = it[1] val relay = it.getOrNull(2) @@ -76,8 +76,12 @@ open class Event( override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex } + override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex } + override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex } + override fun isTaggedAddressableNotes(idHexes: Set) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes } + override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) } override fun isTaggedHashes(hashtags: Set) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags } override fun firstIsTaggedHashes(hashtags: Set) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt index 8ca18b031..7087d183a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventInterface.kt @@ -27,7 +27,11 @@ interface EventInterface { fun hasValidSignature(): Boolean fun isTaggedUser(idHex: String): Boolean + + fun isTaggedEvent(idHex: String): Boolean + fun isTaggedAddressableNote(idHex: String): Boolean + fun isTaggedAddressableNotes(idHexes: Set): Boolean fun isTaggedHash(hashtag: String): Boolean fun isTaggedHashes(hashtags: Set): Boolean @@ -46,6 +50,11 @@ interface EventInterface { fun isSensitive(): Boolean fun zapraiserAmount(): Long? + fun taggedAddresses(): List + fun taggedUsers(): List + fun taggedEvents(): List + fun taggedUrls(): List + fun taggedEmojis(): List fun matchTag1With(text: String): Boolean } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt index 2594c74f5..fe7c599d3 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GeneralListEvent.kt @@ -54,7 +54,13 @@ abstract class GeneralListEvent( return privateTagsCache } + fun privateTagsOrEmpty(privKey: ByteArray): List> { + return privateTags(privKey) ?: emptyList() + } + fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } + + fun privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] } fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] } fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull { val aTagValue = it.getOrNull(1) @@ -90,5 +96,16 @@ abstract class GeneralListEvent( pubKey ) } + + fun encryptTags( + privateTags: List>? = null, + privateKey: ByteArray + ): String { + return Utils.encrypt( + msg = gson.toJson(privateTags), + privateKey = privateKey, + pubKey = Utils.pubkeyCreate(privateKey) + ) + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt index 13921deee..56c382894 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEventInterface.kt @@ -14,8 +14,6 @@ interface LnZapEventInterface : EventInterface { fun zappedRequestAuthor(): String? - fun taggedAddresses(): List - fun amount(): BigDecimal? fun containedPost(): Event? diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt index 69e4a7f31..751c4630d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt @@ -4,6 +4,9 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.TimeUtils import com.vitorpamplona.amethyst.model.toHexKey +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toImmutableSet import nostr.postr.Utils @Immutable @@ -15,42 +18,133 @@ class PeopleListEvent( content: String, sig: HexKey ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { + var publicAndPrivateUserCache: ImmutableSet? = null + + fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet { + publicAndPrivateUserCache?.let { + return it + } + + val privateUserList = privateKey?.let { + privateTagsOrEmpty(privKey = it).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet() + } ?: emptySet() + val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet() + + publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet() + + return publicAndPrivateUserCache ?: persistentSetOf() + } + + fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray): Boolean { + return if (isPrivate) { + privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == "p" && it[1] == idHex } + } else { + isTaggedUser(idHex) + } + } + companion object { const val kind = 30000 + const val blockList = "mute" - fun create( - name: String = "", - - events: List? = null, - users: List? = null, - addresses: List? = null, - - privEvents: List? = null, - privUsers: List? = null, - privAddresses: List? = null, - - privateKey: ByteArray, - createdAt: Long = TimeUtils.now() - ): PeopleListEvent { - val pubKey = Utils.pubkeyCreate(privateKey) - val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey) - - val tags = mutableListOf>() - tags.add(listOf("d", name)) - - events?.forEach { - tags.add(listOf("e", it)) - } - users?.forEach { - tags.add(listOf("p", it)) - } - addresses?.forEach { - tags.add(listOf("a", it.toTag())) + fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { + return if (isPrivate) { + create( + content = encryptTags(listOf(listOf("p", pubKeyHex)), privateKey), + tags = listOf(listOf("d", name)), + privateKey = privateKey, + createdAt = createdAt + ) + } else { + create( + content = "", + tags = listOf(listOf("d", name), listOf("p", pubKeyHex)), + privateKey = privateKey, + createdAt = createdAt + ) } + } - val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content) + fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { + return if (isPrivate) { + create( + content = encryptTags( + privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus( + listPubKeyHex.map { + listOf("p", it) + } + ), + privateKey = privateKey + ), + tags = earlierVersion.tags, + privateKey = privateKey, + createdAt = createdAt + ) + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus( + listPubKeyHex.map { + listOf("p", it) + } + ), + privateKey = privateKey, + createdAt = createdAt + ) + } + } + + fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { + if (earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion + + return if (isPrivate) { + create( + content = encryptTags( + privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(element = listOf("p", pubKeyHex)), + privateKey = privateKey + ), + tags = earlierVersion.tags, + privateKey = privateKey, + createdAt = createdAt + ) + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)), + privateKey = privateKey, + createdAt = createdAt + ) + } + } + + fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { + if (!earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion + + return if (isPrivate) { + create( + content = encryptTags( + privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).filter { it.size > 1 && it[1] != pubKeyHex }, + privateKey = privateKey + ), + tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex }, + privateKey = privateKey, + createdAt = createdAt + ) + } else { + create( + content = earlierVersion.content, + tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex }, + privateKey = privateKey, + createdAt = createdAt + ) + } + } + + fun create(content: String, tags: List>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return PeopleListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey()) + return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt index 4fb04ee14..6d2c341f7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/ChatroomListKnownFeedFilter.kt @@ -32,7 +32,9 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter + val publicChannels = account.selectedChatsFollowList().mapNotNull { + LocalCache.getChannelIfExists(it) + }.mapNotNull { it -> it.notes.values .filter { account.isAcceptable(it) && it.event != null } .sortedWith(compareBy({ it.createdAt() }, { it.idHex })) @@ -40,7 +42,6 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter, account: Account): MutableMap { - val followingChannels = account.followingChannels + val followingChannels = account.userProfile().latestContactList?.taggedEvents()?.toSet() ?: emptySet() val newRelevantPublicMessages = mutableMapOf() newItems.filter { it.event is ChannelMessageEvent }.forEach { newNote -> newNote.channelHex()?.let { channelHex -> 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 ca6fd4b80..1990b7d47 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 @@ -13,6 +13,10 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter { val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) } @@ -28,6 +32,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter): Set { val now = TimeUtils.now() val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS + val isHiddenList = showHiddenKey() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet() @@ -36,15 +41,15 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter { val allNotes = LocalCache.addressables.values @@ -28,15 +32,24 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte protected open fun innerApplyFilter(collection: Collection): Set { val now = TimeUtils.now() val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS + val isHiddenList = showHiddenKey() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet() - val activities = collection + val createEvents = collection.filter { it.event is CommunityDefinitionEvent } + val anyOtherCommunityEvent = collection + .asSequence() + .filter { it.event is CommunityPostApprovalEvent } + .mapNotNull { (it.event as? CommunityPostApprovalEvent)?.communities() }.flatten() + .map { LocalCache.getOrCreateAddressableNote(it) } + .toSet() + + val activities = (createEvents + anyOtherCommunityEvent) .asSequence() .filter { it.event is CommunityDefinitionEvent } .filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true } - .filter { it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } + .filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true } .filter { (it.createdAt() ?: 0) <= now } .toSet() 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 a53667f69..f470ed1dc 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 @@ -16,6 +16,10 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter { val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) } @@ -33,6 +37,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter): Set { val now = TimeUtils.now() val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS + val isHiddenList = showHiddenKey() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() @@ -47,7 +52,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter { * Returns a string that serves as the key to invalidate the list if it changes. */ abstract fun feedKey(): String + open fun showHiddenKey(): Boolean = false abstract 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 9532df08a..8d286a12d 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 @@ -9,8 +9,28 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter() { return account.userProfile().pubkeyHex } + override fun showHiddenKey(): Boolean { + return true + } + override fun feed(): List { - return (account.hiddenUsers + account.transientHiddenUsers) - .map { LocalCache.getOrCreateUser(it) } + return account.getBlockList() + ?.publicAndPrivateUsers(account.loggedIn.privKey) + ?.map { LocalCache.getOrCreateUser(it) } + ?: emptyList() + } +} + +class SpammerAccountsFeedFilter(val account: Account) : FeedFilter() { + override fun feedKey(): String { + return account.userProfile().pubkeyHex + } + + override fun showHiddenKey(): Boolean { + return true + } + + override fun feed(): List { + return (account.transientHiddenUsers).map { LocalCache.getOrCreateUser(it) } } } 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 af5686bde..d977d0d46 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 @@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.TimeUtils import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent +import com.vitorpamplona.amethyst.service.model.PeopleListEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent @@ -16,6 +17,10 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter { return sort(innerApplyFilter(LocalCache.notes.values)) } @@ -26,6 +31,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter): Set { val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS + val isHiddenList = showHiddenKey() val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() @@ -38,7 +44,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter() return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList } + override fun showHiddenKey(): Boolean { + return account.defaultHomeFollowList == PeopleListEvent.blockList + } + override fun feed(): List { val notes = innerApplyFilter(LocalCache.notes.values) val longFormNotes = innerApplyFilter(LocalCache.addressables.values) @@ -32,9 +37,11 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() private fun innerApplyFilter(collection: Collection): Set { val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS + val isHiddenList = showHiddenKey() val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() + val followingCommunities = account.selectedCommunitiesFollowList(account.defaultHomeFollowList) ?: emptySet() val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future. val oneHr = 60 * 60 @@ -44,9 +51,9 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() .filter { it -> val noteEvent = it.event (noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) && - (isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet)) && + (isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) && // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable - it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true && + (isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) && ((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) && it.isNewThread() && ( 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 8b303e718..e07642327 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 @@ -12,6 +12,10 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() return account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList } + override fun showHiddenKey(): Boolean { + return account.defaultNotificationFollowList == PeopleListEvent.blockList + } + override fun feed(): List { return sort(innerApplyFilter(LocalCache.notes.values)) } @@ -22,6 +26,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() private fun innerApplyFilter(collection: Collection): Set { val isGlobal = account.defaultNotificationFollowList == GLOBAL_FOLLOWS + val isHiddenList = showHiddenKey() val followingKeySet = account.selectedUsersFollowList(account.defaultNotificationFollowList) ?: emptySet() @@ -37,7 +42,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter() it.author !== loggedInUser && (isGlobal || it.author?.pubkeyHex in followingKeySet) && it.event?.isTaggedUser(loggedInUserHex) ?: false && - (it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && + (isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) && tagsAnEventByUser(it, loggedInUserHex) }.toSet() } 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 d8f34198d..7daf16abd 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 @@ -12,6 +12,10 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { return account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList } + override fun showHiddenKey(): Boolean { + return account.defaultStoriesFollowList == PeopleListEvent.blockList + } + override fun feed(): List { val notes = innerApplyFilter(LocalCache.notes.values) @@ -25,6 +29,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { private fun innerApplyFilter(collection: Collection): Set { val now = TimeUtils.now() val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS + val isHiddenList = account.defaultStoriesFollowList == PeopleListEvent.blockList val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet() @@ -33,7 +38,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { .asSequence() .filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent } .filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) } - .filter { account.isAcceptable(it) } + .filter { isHiddenList || account.isAcceptable(it) } .filter { it.createdAt()!! <= now } .toSet() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index 967f918e5..90f7fd6e8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -43,7 +43,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForLastRead: String, showHidden: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by likeSetCard.note.live().metadata.observeAsState() val note = noteState?.note @@ -158,6 +158,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL baseNote = it, routeForLastRead = null, isBoostedNote = true, + showHidden = showHidden, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt index b0264a5e9..c76b48ab4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChannelCardCompose.kt @@ -87,6 +87,7 @@ fun ChannelCardCompose( modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, forceEventKind: Int?, + showHidden: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { @@ -114,6 +115,7 @@ fun ChannelCardCompose( routeForLastRead, modifier, parentBackgroundColor, + showHidden, accountViewModel, nav ) @@ -128,24 +130,47 @@ fun CheckHiddenChannelCardCompose( routeForLastRead: String? = null, modifier: Modifier = Modifier, parentBackgroundColor: MutableState? = null, + showHidden: Boolean, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val isHidden by accountViewModel.accountLiveData.map { - accountViewModel.isNoteHidden(note) - }.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note)) - - Crossfade(targetState = isHidden) { - if (!it) { - LoadedChannelCardCompose( - note, - routeForLastRead, - modifier, - parentBackgroundColor, - accountViewModel, - nav + if (showHidden) { + var state by remember { + mutableStateOf( + NoteComposeReportState( + isAcceptable = true, + canPreview = true, + relevantReports = persistentSetOf() + ) ) } + + RenderChannelCardReportState( + state = state, + note = note, + routeForLastRead = routeForLastRead, + modifier = modifier, + parentBackgroundColor = parentBackgroundColor, + accountViewModel = accountViewModel, + nav = nav + ) + } else { + val isHidden by accountViewModel.account.liveHiddenUsers.map { + note.isHiddenFor(it) + }.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note)) + + Crossfade(targetState = isHidden) { + if (!it) { + LoadedChannelCardCompose( + note, + routeForLastRead, + modifier, + parentBackgroundColor, + accountViewModel, + nav + ) + } + } } } @@ -635,11 +660,11 @@ fun RenderChannelThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val channelUpdates by channel.live.observeAsState() - val name = remember(channelUpdates) { channel.toBestDisplayName() } - val description = remember(channelUpdates) { channel.summary() } + val name = remember(channelUpdates) { channelUpdates?.channel?.toBestDisplayName() ?: "" } + val description = remember(channelUpdates) { channelUpdates?.channel?.summary() } val cover by remember(channelUpdates) { derivedStateOf { - channel.profilePicture()?.ifBlank { null } + channelUpdates?.channel?.profilePicture()?.ifBlank { null } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt index 509b64a3a..dec9417de 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -35,7 +35,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable -fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, showHidden: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val baseNote = remember { messageSetCard.note } val accountState by accountViewModel.accountLiveData.observeAsState() @@ -102,6 +102,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String, routeForLastRead = null, isBoostedNote = true, addMarginTop = false, + showHidden = showHidden, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index 3297f1404..002bb0473 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -32,7 +32,6 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -86,7 +85,7 @@ import kotlin.time.measureTimedValue @OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class) @Composable -fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHidden: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val baseNote = remember { multiSetCard.note } val popupExpanded = remember { mutableStateOf(false) } @@ -154,6 +153,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun routeForLastRead = null, modifier = remember { Modifier.padding(top = 5.dp) }, isBoostedNote = true, + showHidden = showHidden, parentBackgroundColor = backgroundColor, accountViewModel = accountViewModel, nav = nav @@ -562,7 +562,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture( } } - WatchFollows(author.pubkeyHex, accountViewModel) { isFollowing -> + WatchUserFollows(author.pubkeyHex, accountViewModel) { isFollowing -> Crossfade(targetState = isFollowing) { if (it) { Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index eb999a605..87a445930 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -214,6 +214,7 @@ fun NoteCompose( unPackReply: Boolean = true, makeItShort: Boolean = false, addMarginTop: Boolean = true, + showHidden: Boolean = false, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit @@ -245,6 +246,7 @@ fun NoteCompose( unPackReply, makeItShort, addMarginTop, + showHidden, parentBackgroundColor, accountViewModel, nav @@ -263,30 +265,59 @@ fun CheckHiddenNoteCompose( unPackReply: Boolean = true, makeItShort: Boolean = false, addMarginTop: Boolean = true, + showHidden: Boolean = false, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val isHidden by accountViewModel.accountLiveData.map { - accountViewModel.isNoteHidden(note) - }.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note)) - - Crossfade(targetState = isHidden) { - if (!it) { - LoadedNoteCompose( - note, - routeForLastRead, - modifier, - isBoostedNote, - isQuotedNote, - unPackReply, - makeItShort, - addMarginTop, - parentBackgroundColor, - accountViewModel, - nav + if (showHidden) { + // Ignores reports as well + var state by remember { + mutableStateOf( + NoteComposeReportState( + isAcceptable = true, + canPreview = true, + relevantReports = persistentSetOf() + ) ) } + + RenderReportState( + state, + note, + routeForLastRead, + modifier, + isBoostedNote, + isQuotedNote, + unPackReply, + makeItShort, + addMarginTop, + parentBackgroundColor, + accountViewModel, + nav + ) + } else { + val isHidden by accountViewModel.account.liveHiddenUsers.map { + note.isHiddenFor(it) + }.observeAsState(accountViewModel.isNoteHidden(note)) + + Crossfade(targetState = isHidden) { + if (!it) { + LoadedNoteCompose( + note, + routeForLastRead, + modifier, + isBoostedNote, + isQuotedNote, + unPackReply, + makeItShort, + addMarginTop, + parentBackgroundColor, + accountViewModel, + nav + ) + } + } } } @@ -438,13 +469,15 @@ fun NormalNote( accountViewModel = accountViewModel, nav = nav ) - is CommunityDefinitionEvent -> CommunityHeader( - baseNote = baseNote, - showBottomDiviser = true, - sendToCommunity = true, - accountViewModel = accountViewModel, - nav = nav - ) + is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let { + CommunityHeader( + baseNote = it, + showBottomDiviser = true, + sendToCommunity = true, + accountViewModel = accountViewModel, + nav = nav + ) + } is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote) is FileHeaderEvent -> FileHeaderDisplay(baseNote, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, accountViewModel) @@ -471,14 +504,14 @@ fun NormalNote( @Composable fun CommunityHeader( - baseNote: Note, + baseNote: AddressableNote, showBottomDiviser: Boolean, sendToCommunity: Boolean, modifier: Modifier = StdPadding, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - var expanded = remember { mutableStateOf(false) } + val expanded = remember { mutableStateOf(false) } Column( modifier = modifier.clickable { @@ -506,7 +539,7 @@ fun CommunityHeader( } @Composable -fun LongCommunityHeader(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by baseNote.live().metadata.observeAsState() val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return @@ -648,7 +681,7 @@ fun LongCommunityHeader(baseNote: Note, accountViewModel: AccountViewModel, nav: } @Composable -fun ShortCommunityHeader(baseNote: Note, expanded: MutableState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by baseNote.live().metadata.observeAsState() val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return @@ -713,44 +746,45 @@ fun ShortCommunityHeader(baseNote: Note, expanded: MutableState, accoun @Composable private fun ShortCommunityActionOptions( - note: Note, + note: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val isFollowing by remember(accountState) { - derivedStateOf { - accountState?.account?.followingCommunities?.contains(note.idHex) ?: false - } - } - Spacer(modifier = StdHorzSpacer) LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav) Spacer(modifier = StdHorzSpacer) ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel) - if (!isFollowing) { - Spacer(modifier = StdHorzSpacer) - JoinCommunityButton(accountViewModel, note, nav) + WatchAddressableNoteFollows(note, accountViewModel) { isFollowing -> + if (!isFollowing) { + Spacer(modifier = StdHorzSpacer) + JoinCommunityButton(accountViewModel, note, nav) + } } } +@Composable +fun WatchAddressableNoteFollows(note: AddressableNote, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) { + val showFollowingMark by accountViewModel.userFollows.map { + it.user.latestContactList?.isTaggedAddressableNote(note.idHex) ?: false + }.distinctUntilChanged().observeAsState( + accountViewModel.userProfile().latestContactList?.isTaggedAddressableNote(note.idHex) ?: false + ) + + onFollowChanges(showFollowingMark) +} + @Composable private fun LongCommunityActionOptions( - note: Note, + note: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val isFollowing by remember(accountState) { - derivedStateOf { - accountState?.account?.followingCommunities?.contains(note.idHex) ?: false + WatchAddressableNoteFollows(note, accountViewModel) { isFollowing -> + if (isFollowing) { + LeaveCommunityButton(accountViewModel, note, nav) } } - - if (isFollowing) { - LeaveCommunityButton(accountViewModel, note, nav) - } } @Composable diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index 5169a8846..a4a06ab7b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -363,6 +363,20 @@ private fun WatchReactionsZapsBoostsAndDisplayIfExists(baseNote: Note, content: } } +fun LiveData.combineWith( + liveData1: LiveData, + block: (T?, K?) -> R +): LiveData { + val result = MediatorLiveData() + result.addSource(this) { + result.value = block(this.value, liveData1.value) + } + result.addSource(liveData1) { + result.value = block(this.value, liveData1.value) + } + return result +} + fun LiveData.combineWith( liveData1: LiveData, liveData2: LiveData

, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 370f3ff94..47e773821 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -264,7 +264,7 @@ fun PictureAndFollowingMark( @Composable fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) { - WatchFollows(userHex, accountViewModel) { newFollowingState -> + WatchUserFollows(userHex, accountViewModel) { newFollowingState -> Crossfade(targetState = newFollowingState) { following -> if (following) { Box(contentAlignment = Alignment.TopEnd) { @@ -276,7 +276,7 @@ fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewMod } @Composable -fun WatchFollows(userHex: String, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) { +fun WatchUserFollows(userHex: String, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) { val showFollowingMark by accountViewModel.userFollows.map { it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex) }.distinctUntilChanged().observeAsState( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index f3ac34115..019a41e57 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -174,7 +174,9 @@ fun UserActionOptions( ) { val scope = rememberCoroutineScope() val accountState by accountViewModel.accountLiveData.observeAsState() - val isHidden by remember(accountState) { + val blockList by accountViewModel.account.getBlockListNote().live().metadata.observeAsState() + + val isHidden by remember(accountState, blockList) { derivedStateOf { accountState?.account?.isHidden(baseAuthor) ?: false } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index 7bf24b31a..50fda8bed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -141,9 +141,10 @@ fun RenderCardFeed( FeedLoaded( state = state, listState = listState, + routeForLastRead = routeForLastRead, + showHidden = viewModel.showHidden(), accountViewModel = accountViewModel, - nav = nav, - routeForLastRead = routeForLastRead + nav = nav ) } CardFeedState.Loading -> { @@ -158,9 +159,10 @@ fun RenderCardFeed( private fun FeedLoaded( state: CardFeedState.Loaded, listState: LazyListState, + showHidden: Boolean, + routeForLastRead: String, accountViewModel: AccountViewModel, - nav: (String) -> Unit, - routeForLastRead: String + nav: (String) -> Unit ) { LazyColumn( modifier = remember { Modifier.fillMaxSize() }, @@ -179,7 +181,7 @@ private fun FeedLoaded( } Row(defaultModifier) { - RenderCardItem(item, accountViewModel, nav, routeForLastRead) + RenderCardItem(item, routeForLastRead, showHidden, accountViewModel, nav) } } } @@ -188,15 +190,17 @@ private fun FeedLoaded( @Composable private fun RenderCardItem( item: Card, + routeForLastRead: String, + showHidden: Boolean, accountViewModel: AccountViewModel, - nav: (String) -> Unit, - routeForLastRead: String + nav: (String) -> Unit ) { when (item) { is NoteCard -> NoteCompose( item, isBoostedNote = false, accountViewModel = accountViewModel, + showHidden = showHidden, nav = nav, routeForLastRead = routeForLastRead ) @@ -212,6 +216,7 @@ private fun RenderCardItem( is MultiSetCard -> MultiSetCompose( item, accountViewModel = accountViewModel, + showHidden = showHidden, nav = nav, routeForLastRead = routeForLastRead ) @@ -219,6 +224,7 @@ private fun RenderCardItem( is BadgeCard -> BadgeCompose( item, accountViewModel = accountViewModel, + showHidden = showHidden, nav = nav, routeForLastRead = routeForLastRead ) @@ -226,6 +232,7 @@ private fun RenderCardItem( is MessageSetCard -> MessageSetCompose( messageSetCard = item, routeForLastRead = routeForLastRead, + showHidden = showHidden, accountViewModel = accountViewModel, nav = nav ) @@ -242,6 +249,7 @@ fun NoteCompose( unPackReply: Boolean = true, makeItShort: Boolean = false, addMarginTop: Boolean = true, + showHidden: Boolean = false, parentBackgroundColor: MutableState? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit @@ -259,6 +267,7 @@ fun NoteCompose( unPackReply = unPackReply, makeItShort = makeItShort, addMarginTop = addMarginTop, + showHidden = showHidden, parentBackgroundColor = parentBackgroundColor, accountViewModel = accountViewModel, nav = nav diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 0b7def101..26a0d3fc9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -71,6 +71,10 @@ open class CardFeedViewModel(val localFilter: FeedFilter) : ViewModel() { scrolltoTopPending = false } + fun showHidden(): Boolean { + return localFilter.showHiddenKey() + } + private var lastAccount: Account? = null private var lastNotes: Set? = null diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt index 0d14e3596..e37af1f68 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedView.kt @@ -151,6 +151,7 @@ private fun RenderFeed( state, listState, routeForLastRead, + viewModel.showHidden(), accountViewModel, nav ) @@ -184,6 +185,7 @@ private fun FeedLoaded( state: FeedState.Loaded, listState: LazyListState, routeForLastRead: String?, + showHidden: Boolean = false, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { @@ -210,6 +212,7 @@ private fun FeedLoaded( routeForLastRead = routeForLastRead, modifier = baseModifier, isBoostedNote = false, + showHidden = showHidden, accountViewModel = accountViewModel, nav = nav ) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index c13af964c..bf06ef18d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -217,6 +217,10 @@ abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel(), I scrolltoTopPending = false } + fun showHidden(): Boolean { + return localFilter.showHiddenKey() + } + private fun refresh() { val scope = CoroutineScope(Job() + Dispatchers.Default) scope.launch { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index c73335dd9..a578f123f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -13,6 +13,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter +import com.vitorpamplona.amethyst.ui.dal.SpammerAccountsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter import kotlinx.collections.immutable.ImmutableList @@ -49,6 +50,14 @@ class NostrHiddenAccountsFeedViewModel(val account: Account) : UserFeedViewModel } } +class NostrSpammerAccountsFeedViewModel(val account: Account) : UserFeedViewModel(SpammerAccountsFeedFilter(account)) { + class Factory(val account: Account) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrSpammerAccountsFeedViewModel { + return NostrSpammerAccountsFeedViewModel(account) as NostrSpammerAccountsFeedViewModel + } + } +} + @Stable open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel(), InvalidatableViewModel { private val _feedContent = MutableStateFlow(UserFeedState.Loading) @@ -61,6 +70,10 @@ open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel(), In } } + fun showHidden(): Boolean { + return dataSource.showHiddenKey() + } + private fun refreshSuspended() { checkNotInMainThread() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index b3cfe50ad..70f91e3cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -80,6 +80,7 @@ import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.AddressableNote import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache @@ -863,13 +864,6 @@ private fun ShortChannelActionOptions( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - val accountState by accountViewModel.accountLiveData.observeAsState() - val isFollowing by remember(accountState) { - derivedStateOf { - accountState?.account?.followingChannels?.contains(channel.idHex) ?: false - } - } - LoadNote(baseNoteHex = channel.idHex) { it?.let { var popupExpanded by remember { mutableStateOf(false) } @@ -882,11 +876,28 @@ private fun ShortChannelActionOptions( } } - if (!isFollowing) { - JoinChatButton(accountViewModel, channel, nav) + WatchChannelFollows(channel, accountViewModel) { isFollowing -> + if (!isFollowing) { + JoinChatButton(accountViewModel, channel, nav) + } } } +@Composable +private fun WatchChannelFollows( + channel: PublicChatChannel, + accountViewModel: AccountViewModel, + content: @Composable (Boolean) -> Unit +) { + val isFollowing by accountViewModel.userProfile().live().follows.map { + it.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false + }.distinctUntilChanged().observeAsState( + accountViewModel.userProfile().latestContactList?.isTaggedEvent(channel.idHex) ?: false + ) + + content(isFollowing) +} + @Composable private fun LongChannelActionOptions( channel: PublicChatChannel, @@ -899,19 +910,14 @@ private fun LongChannelActionOptions( } } - val accountState by accountViewModel.accountLiveData.observeAsState() - val isFollowing by remember(accountState) { - derivedStateOf { - accountState?.account?.followingChannels?.contains(channel.idHex) ?: false - } - } - if (isMe) { EditButton(accountViewModel, channel) } - if (isFollowing) { - LeaveChatButton(accountViewModel, channel, nav) + WatchChannelFollows(channel, accountViewModel) { isFollowing -> + if (isFollowing) { + LeaveChatButton(accountViewModel, channel, nav) + } } } @@ -1074,10 +1080,14 @@ private fun EditButton(accountViewModel: AccountViewModel, channel: PublicChatCh @Composable fun JoinChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) { + val scope = rememberCoroutineScope() + Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - accountViewModel.account.joinChannel(channel.idHex) + scope.launch(Dispatchers.IO) { + accountViewModel.account.follow(channel) + } }, shape = ButtonBorder, colors = ButtonDefaults @@ -1092,10 +1102,14 @@ fun JoinChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (S @Composable fun LeaveChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) { + val scope = rememberCoroutineScope() + Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - accountViewModel.account.leaveChannel(channel.idHex) + scope.launch(Dispatchers.IO) { + accountViewModel.account.unfollow(channel) + } }, shape = ButtonBorder, colors = ButtonDefaults @@ -1109,11 +1123,15 @@ fun LeaveChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: ( } @Composable -fun JoinCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (String) -> Unit) { +fun JoinCommunityButton(accountViewModel: AccountViewModel, note: AddressableNote, nav: (String) -> Unit) { + val scope = rememberCoroutineScope() + Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - accountViewModel.account.joinCommunity(note.idHex) + scope.launch(Dispatchers.IO) { + accountViewModel.account.follow(note) + } }, shape = ButtonBorder, colors = ButtonDefaults @@ -1127,11 +1145,15 @@ fun JoinCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (St } @Composable -fun LeaveCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (String) -> Unit) { +fun LeaveCommunityButton(accountViewModel: AccountViewModel, note: AddressableNote, nav: (String) -> Unit) { + val scope = rememberCoroutineScope() + Button( modifier = Modifier.padding(horizontal = 3.dp), onClick = { - accountViewModel.account.leaveCommunity((note.idHex)) + scope.launch(Dispatchers.IO) { + accountViewModel.account.unfollow(note) + } }, shape = ButtonBorder, colors = ButtonDefaults 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 11fecfe6b..5d68bf472 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 @@ -32,23 +32,30 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView +import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import kotlinx.coroutines.launch @Composable fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) { - val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel( + val hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel = viewModel( factory = NostrHiddenAccountsFeedViewModel.Factory(accountViewModel.account) ) - HiddenUsersScreen(feedViewModel, accountViewModel, nav) + val spammerFeedViewModel: NostrSpammerAccountsFeedViewModel = viewModel( + factory = NostrSpammerAccountsFeedViewModel.Factory(accountViewModel.account) + ) + + HiddenUsersScreen(hiddenFeedViewModel, spammerFeedViewModel, accountViewModel, nav) } @OptIn(ExperimentalFoundationApi::class) @Composable fun HiddenUsersScreen( - feedViewModel: NostrHiddenAccountsFeedViewModel, + hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel, + spammerFeedViewModel: NostrSpammerAccountsFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { @@ -58,10 +65,8 @@ fun HiddenUsersScreen( val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { println("Hidden Users Start") - feedViewModel.invalidateData() - } - if (event == Lifecycle.Event.ON_PAUSE) { - println("Hidden Users Stop") + hiddenFeedViewModel.invalidateData() + spammerFeedViewModel.invalidateData() } } @@ -116,10 +121,18 @@ fun HiddenUsersScreen( Text(text = stringResource(R.string.blocked_users)) } ) + Tab( + selected = pagerState.currentPage == 1, + onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, + text = { + Text(text = stringResource(R.string.spamming_users)) + } + ) } - HorizontalPager(pageCount = 1, state = pagerState) { page -> + HorizontalPager(pageCount = 2, state = pagerState) { page -> when (page) { - 0 -> RefreshingUserFeedView(feedViewModel, accountViewModel, nav) + 0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel, nav) + 1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel, nav) } } } @@ -128,22 +141,23 @@ fun HiddenUsersScreen( @Composable fun RefreshingUserFeedView( - feedViewModel: NostrHiddenAccountsFeedViewModel, + feedViewModel: UserFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - WatchAccount(feedViewModel, accountViewModel) + WatchAccountAndBlockList(feedViewModel, accountViewModel) RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav) } @Composable -fun WatchAccount( - feedViewModel: NostrHiddenAccountsFeedViewModel, +fun WatchAccountAndBlockList( + feedViewModel: UserFeedViewModel, accountViewModel: AccountViewModel ) { val accountState by accountViewModel.accountLiveData.observeAsState() + val blockListState by accountViewModel.account.getBlockListNote().live().metadata.observeAsState() - LaunchedEffect(accountViewModel, accountState) { + LaunchedEffect(accountViewModel, accountState, blockListState) { feedViewModel.invalidateData() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index b52a52e68..95ab61bf5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -701,45 +701,48 @@ private fun ProfileHeader( private fun ProfileActions( baseUser: User, accountViewModel: AccountViewModel +) { + val isMe by remember(accountViewModel) { + derivedStateOf { + accountViewModel.userProfile() == baseUser + } + } + + if (isMe) { + EditButton(accountViewModel.account) + } + + WatchIsHiddenUser(baseUser, accountViewModel) { isHidden -> + if (isHidden) { + val scope = rememberCoroutineScope() + ShowUserButton { + scope.launch(Dispatchers.IO) { + accountViewModel.account.showUser(baseUser.pubkeyHex) + } + } + } else { + DisplayFollowUnfollowButton(baseUser, accountViewModel) + } + } +} + +@Composable +private fun DisplayFollowUnfollowButton( + baseUser: User, + accountViewModel: AccountViewModel ) { val scope = rememberCoroutineScope() val context = LocalContext.current - val accountLocalUserState by accountViewModel.accountLiveData.observeAsState() - val account = remember(accountLocalUserState) { accountLocalUserState?.account } ?: return + val isLoggedInFollowingUser by accountViewModel.account.userProfile().live().follows.map { + it.user.isFollowing(baseUser) + }.distinctUntilChanged().observeAsState(initial = accountViewModel.account.isFollowing(baseUser)) - val accountUserState by accountViewModel.account.userProfile().live().follows.observeAsState() - val baseUserState by baseUser.live().follows.observeAsState() + val isUserFollowingLoggedIn by baseUser.live().follows.map { + it.user.isFollowing(accountViewModel.account.userProfile()) + }.distinctUntilChanged().observeAsState(initial = baseUser.isFollowing(accountViewModel.account.userProfile())) - val accountUser = remember(accountUserState) { accountUserState?.user } ?: return - - val isHidden by remember(accountUserState, accountLocalUserState) { - derivedStateOf { - account.isHidden(baseUser) - } - } - - val isLoggedInFollowingUser by remember(accountUserState, accountLocalUserState) { - derivedStateOf { - accountUser.isFollowingCached(baseUser) - } - } - - val isUserFollowingLoggedIn by remember(baseUserState, accountLocalUserState) { - derivedStateOf { - baseUser.isFollowing(accountUser) - } - } - - if (accountUser == baseUser) { - EditButton(account) - } - - if (isHidden) { - ShowUserButton { - account.showUser(baseUser.pubkeyHex) - } - } else if (isLoggedInFollowingUser) { + if (isLoggedInFollowingUser) { UnfollowButton { if (!accountViewModel.isWriteable()) { scope.launch { @@ -753,7 +756,7 @@ private fun ProfileActions( } } else { scope.launch(Dispatchers.IO) { - account.unfollow(baseUser) + accountViewModel.account.unfollow(baseUser) } } } @@ -772,7 +775,7 @@ private fun ProfileActions( } } else { scope.launch(Dispatchers.IO) { - account.follow(baseUser) + accountViewModel.account.follow(baseUser) } } } @@ -790,7 +793,7 @@ private fun ProfileActions( } } else { scope.launch(Dispatchers.IO) { - account.follow(baseUser) + accountViewModel.account.follow(baseUser) } } } @@ -798,6 +801,15 @@ private fun ProfileActions( } } +@Composable +private fun WatchIsHiddenUser(baseUser: User, accountViewModel: AccountViewModel, content: @Composable (Boolean) -> Unit) { + val isHidden by accountViewModel.account.liveHiddenUsers.map { + it.hiddenUsers.contains(baseUser.pubkeyHex) || it.spammers.contains(baseUser.pubkeyHex) + }.observeAsState(accountViewModel.account.isHidden(baseUser)) + + content(isHidden) +} + @Composable private fun DrawAdditionalInfo( baseUser: User, @@ -1638,6 +1650,7 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () -> val clipboardManager = LocalClipboardManager.current val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account!! + val blockList by accountViewModel.account.getBlockListNote().live().metadata.observeAsState() val scope = rememberCoroutineScope() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8a1ca7b2b..6b3513144 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -494,4 +494,6 @@ Automatically play videos Automatically show url preview Load Image + + Spammers