Lot's of changes from local storage to relay storage.

- Moves Following Communities local DB to Contact List
- Moves Following Channels local DB to Contact List
- Moves Following BlockList local DB to Mute List (Private part)
- Migrates all deprecated local lists to event kinds.
- Views Mute Feed (disables hidden authors for that specific list)
- Breaks Security Filters screen in 2 tabs: Blocked and Spammers (automated filter)
- Restructures PeopleList event kind
- Fixes older channels and communities not loading on the discovery tab
This commit is contained in:
Vitor Pamplona
2023-07-16 18:57:28 -04:00
parent 4a2dced02f
commit d78c7a91cb
39 changed files with 1039 additions and 315 deletions
@@ -6,6 +6,8 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.core.os.ConfigurationCompat import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.OptOutFromFilters import com.vitorpamplona.amethyst.OptOutFromFilters
import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource 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.actions.ServersAvailable
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.note.Nip47URI 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.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -49,9 +55,11 @@ val KIND3_FOLLOWS = " All Follows "
@Stable @Stable
class Account( class Account(
val loggedIn: Persona, val loggedIn: Persona,
var followingChannels: Set<String> = DefaultChannels,
var followingCommunities: Set<String> = setOf(), var followingChannels: Set<String> = DefaultChannels, // deprecated
var hiddenUsers: Set<String> = setOf(), var followingCommunities: Set<String> = setOf(), // deprecated
var hiddenUsers: Set<String> = setOf(), // deprecated
var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(), var localRelays: Set<RelaySetupInfo> = Constants.defaultRelays.toSet(),
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(), var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
var languagePreferences: Map<String, String> = mapOf(), var languagePreferences: Map<String, String> = mapOf(),
@@ -76,7 +84,7 @@ class Account(
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(), var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
var settings: Settings = Settings() var settings: Settings = Settings()
) { ) {
var transientHiddenUsers: Set<String> = setOf() var transientHiddenUsers: ImmutableSet<String> = persistentSetOf()
// Observers line up here. // Observers line up here.
val live: AccountLiveData = AccountLiveData(this) val live: AccountLiveData = AccountLiveData(this)
@@ -84,7 +92,24 @@ class Account(
val liveLastRead: AccountLiveData = AccountLiveData(this) val liveLastRead: AccountLiveData = AccountLiveData(this)
val saveable: AccountLiveData = AccountLiveData(this) val saveable: AccountLiveData = AccountLiveData(this)
@Immutable
data class LiveHiddenUsers(
val hiddenUsers: ImmutableSet<String>,
val spammers: ImmutableSet<String>,
val showSensitiveContent: Boolean?
)
val liveHiddenUsers: LiveData<LiveHiddenUsers> = 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 var userProfileCache: User? = null
fun updateAutomaticallyStartPlayback( fun updateAutomaticallyStartPlayback(
automaticallyStartPlayback: Boolean? automaticallyStartPlayback: Boolean?
) { ) {
@@ -114,7 +139,7 @@ class Account(
filterSpamFromStrangers = filterSpam filterSpamFromStrangers = filterSpam
OptOutFromFilters.start(warnAboutPostsWithReports, filterSpamFromStrangers) OptOutFromFilters.start(warnAboutPostsWithReports, filterSpamFromStrangers)
if (!filterSpamFromStrangers) { if (!filterSpamFromStrangers) {
transientHiddenUsers = setOf() transientHiddenUsers = persistentSetOf()
} }
live.invalidateData() live.invalidateData()
saveable.invalidateData() saveable.invalidateData()
@@ -128,14 +153,6 @@ class Account(
} }
} }
fun followingChannels(): List<Channel> {
return followingChannels.mapNotNull { LocalCache.checkGetOrCreateChannel(it) }
}
fun followingCommunities(): List<AddressableNote> {
return followingCommunities.mapNotNull { LocalCache.checkGetOrCreateAddressableNote(it) }
}
fun isWriteable(): Boolean { fun isWriteable(): Boolean {
return loggedIn.privKey != null return loggedIn.privKey != null
} }
@@ -144,21 +161,25 @@ class Account(
if (!isWriteable()) return if (!isWriteable()) return
val contactList = userProfile().latestContactList val contactList = userProfile().latestContactList
val follows = contactList?.follows() ?: emptyList()
val followsTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
if (contactList != null && follows.isNotEmpty()) { if (contactList != null && contactList.tags.isNotEmpty()) {
val event = ContactListEvent.create( val event = ContactListEvent.updateRelayList(
follows, earlierVersion = contactList,
followsTags, relayUse = relays,
relays, privateKey = loggedIn.privKey!!
loggedIn.privKey!!
) )
Client.send(event) Client.send(event)
LocalCache.consume(event) LocalCache.consume(event)
} else { } 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. // Keep this local to avoid erasing a good contact list.
// Client.send(event) // 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) { fun follow(user: User) {
if (!isWriteable()) return if (!isWriteable()) return
val contactList = userProfile().latestContactList val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val followingUsers = contactList?.follows() ?: emptyList()
val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
val event = if (contactList != null) { val event = if (contactList != null) {
ContactListEvent.create( ContactListEvent.followUser(contactList, user.pubkeyHex, loggedIn.privKey!!)
followingUsers.plus(Contact(user.pubkeyHex, null)), } else {
followingTags, ContactListEvent.createFromScratch(
contactList.relays(), followUsers = listOf(Contact(user.pubkeyHex, null)),
loggedIn.privKey!! 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 { } else {
val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) }
ContactListEvent.create( ContactListEvent.createFromScratch(
listOf(Contact(user.pubkeyHex, null)), followUsers = emptyList(),
followingTags, followTags = emptyList(),
relays, followCommunities = listOf(community.address),
loggedIn.privKey!! followEvents = DefaultChannels.toList(),
relayUse = relays,
privateKey = loggedIn.privKey!!
) )
} }
@@ -429,24 +513,22 @@ class Account(
fun follow(tag: String) { fun follow(tag: String) {
if (!isWriteable()) return if (!isWriteable()) return
val contactList = userProfile().latestContactList val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val followingUsers = contactList?.follows() ?: emptyList()
val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
val event = if (contactList != null) { val event = if (contactList != null) {
ContactListEvent.create( ContactListEvent.followHashtag(
followingUsers, contactList,
followingTags.plus(tag), tag,
contactList.relays(),
loggedIn.privKey!! loggedIn.privKey!!
) )
} else { } else {
val relays = Constants.defaultRelays.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } ContactListEvent.createFromScratch(
ContactListEvent.create( followUsers = emptyList(),
followingUsers, followTags = listOf(tag),
followingTags.plus(tag), followCommunities = emptyList(),
relays, followEvents = DefaultChannels.toList(),
loggedIn.privKey!! 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) { fun unfollow(user: User) {
if (!isWriteable()) return if (!isWriteable()) return
val contactList = userProfile().latestContactList val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val followingUsers = contactList?.follows() ?: emptyList()
val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) { if (contactList != null && contactList.tags.isNotEmpty()) {
val event = ContactListEvent.create( val event = ContactListEvent.unfollowUser(
followingUsers.filter { it.pubKeyHex != user.pubkeyHex }, contactList,
followingTags, user.pubkeyHex,
contactList.relays(),
loggedIn.privKey!! loggedIn.privKey!!
) )
@@ -477,15 +556,46 @@ class Account(
fun unfollow(tag: String) { fun unfollow(tag: String) {
if (!isWriteable()) return if (!isWriteable()) return
val contactList = userProfile().latestContactList val contactList = migrateCommunitiesAndChannelsIfNeeded(userProfile().latestContactList)
val followingUsers = contactList?.follows() ?: emptyList()
val followingTags = contactList?.unverifiedFollowTagSet() ?: emptyList()
if (contactList != null && (followingUsers.isNotEmpty() || followingTags.isNotEmpty())) { if (contactList != null && contactList.tags.isNotEmpty()) {
val event = ContactListEvent.create( val event = ContactListEvent.unfollowHashtag(
followingUsers, contactList,
followingTags.filter { !it.equals(tag, ignoreCase = true) }, tag,
contactList.relays(), 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!! loggedIn.privKey!!
) )
@@ -708,7 +818,9 @@ class Account(
Client.send(event) Client.send(event)
LocalCache.consume(event) LocalCache.consume(event)
joinChannel(event.id) LocalCache.getChannelIfExists(event.id)?.let {
follow(it)
}
} }
fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) { fun removeEmojiPack(usersEmojiList: Note, emojiList: Note) {
@@ -932,43 +1044,70 @@ class Account(
} }
} }
fun joinChannel(idHex: String) { fun getBlockListNote(): AddressableNote {
followingChannels = followingChannels + idHex val aTag = ATag(PeopleListEvent.kind, userProfile().pubkeyHex, PeopleListEvent.blockList, null)
live.invalidateData() return LocalCache.getOrCreateAddressableNote(aTag)
saveable.invalidateData()
} }
fun leaveChannel(idHex: String) { fun getBlockList(): PeopleListEvent? {
followingChannels = followingChannels - idHex return getBlockListNote().event as? PeopleListEvent
live.invalidateData()
saveable.invalidateData()
} }
fun joinCommunity(idHex: String) { private fun migrateHiddenUsersIfNeeded(latestList: PeopleListEvent?): PeopleListEvent? {
followingCommunities = followingCommunities + idHex if (latestList == null) return latestList
live.invalidateData()
saveable.invalidateData() var returningList: PeopleListEvent = latestList
}
fun leaveCommunity(idHex: String) { if (hiddenUsers.isNotEmpty()) {
followingCommunities = followingCommunities - idHex returningList = PeopleListEvent.addUsers(returningList, hiddenUsers.toList(), true, loggedIn.privKey!!)
live.invalidateData() hiddenUsers = emptySet()
}
saveable.invalidateData() return returningList
} }
fun hideUser(pubkeyHex: String) { 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() live.invalidateData()
saveable.invalidateData() saveable.invalidateData()
} }
fun showUser(pubkeyHex: String) { fun showUser(pubkeyHex: String) {
hiddenUsers = hiddenUsers - pubkeyHex val blockList = migrateHiddenUsersIfNeeded(getBlockList())
transientHiddenUsers = transientHiddenUsers - pubkeyHex
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() live.invalidateData()
saveable.invalidateData() saveable.invalidateData()
} }
@@ -1055,7 +1194,53 @@ class Account(
if (listName == GLOBAL_FOLLOWS) return null if (listName == GLOBAL_FOLLOWS) return null
if (listName == KIND3_FOLLOWS) return userProfile().cachedFollowingTagSet() 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<String>? {
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<String> {
val contactList = userProfile().latestContactList
return contactList?.taggedEvents()?.toSet() ?: DefaultChannels
} }
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) { fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
@@ -1076,7 +1261,7 @@ class Account(
Client.send(event) Client.send(event)
LocalCache.consume(event) LocalCache.consume(event)
joinChannel(event.id) follow(channel)
} }
fun decryptContent(note: Note): String? { fun decryptContent(note: Note): String? {
@@ -1224,7 +1409,11 @@ class Account(
} }
fun isHidden(user: User) = isHidden(user.pubkeyHex) 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<HexKey> { fun followingKeySet(): Set<HexKey> {
return userProfile().cachedFollowingKeySet() return userProfile().cachedFollowingKeySet()
@@ -1343,7 +1532,7 @@ class Account(
it.cache.spamMessages.snapshot().values.forEach { it.cache.spamMessages.snapshot().values.forEach {
if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) { if (it.pubkeyHex !in transientHiddenUsers && it.duplicatedMessages.size >= 5) {
if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) { if (it.pubkeyHex != userProfile().pubkeyHex && it.pubkeyHex !in followingKeySet()) {
transientHiddenUsers = transientHiddenUsers + it.pubkeyHex transientHiddenUsers = (transientHiddenUsers + it.pubkeyHex).toImmutableSet()
live.invalidateData() live.invalidateData()
} }
} }
@@ -1356,6 +1545,7 @@ class Account(
Log.d("Init", "Account") Log.d("Init", "Account")
backupContactList?.let { backupContactList?.let {
println("Loading saved contacts ${it.toJson()}") println("Loading saved contacts ${it.toJson()}")
if (userProfile().latestContactList == null) { if (userProfile().latestContactList == null) {
LocalCache.consume(it) LocalCache.consume(it)
} }
@@ -33,7 +33,7 @@ object LocalCache {
ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10) ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
fun checkGetOrCreateUser(key: String): User? { fun checkGetOrCreateUser(key: String): User? {
checkNotInMainThread() // checkNotInMainThread()
if (isValidHex(key)) { if (isValidHex(key)) {
return getOrCreateUser(key) return getOrCreateUser(key)
@@ -143,7 +143,7 @@ object LocalCache {
} }
fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote { fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote {
checkNotInMainThread() // checkNotInMainThread()
// we can't use naddr here because naddr might include relay info and // we can't use naddr here because naddr might include relay info and
// the preferred relay should not be part of the index. // the preferred relay should not be part of the index.
@@ -563,6 +563,15 @@ open class Note(val idHex: String) {
liveSet = null 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) { class NoteLiveSet(u: Note) {
@@ -275,6 +275,10 @@ class User(val pubkeyHex: String) {
return latestContactList?.verifiedFollowTagSet ?: emptySet() return latestContactList?.verifiedFollowTagSet ?: emptySet()
} }
fun cachedFollowingCommunitiesSet(): Set<HexKey> {
return latestContactList?.verifiedFollowCommunitySet ?: emptySet()
}
fun cachedFollowCount(): Int? { fun cachedFollowCount(): Int? {
return latestContactList?.verifiedFollowKeySet?.size return latestContactList?.verifiedFollowKeySet?.size
} }
@@ -44,17 +44,23 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
) )
) )
fun createMyChannelsFilter() = TypedFilter( fun createMyChannelsFilter(): TypedFilter {
types = COMMON_FEED_TYPES, // Metadata comes from any relay val followingEvents = account.selectedChatsFollowList()
filter = JsonFilter(
kinds = listOf(ChannelCreateEvent.kind),
ids = account.followingChannels.toList(),
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
)
)
fun createLastChannelInfoFilter(): List<TypedFilter> { return TypedFilter(
return account.followingChannels.map { 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<TypedFilter>? {
val followingEvents = account.selectedChatsFollowList()
return followingEvents.map {
TypedFilter( TypedFilter(
types = COMMON_FEED_TYPES, // Metadata comes from any relay types = COMMON_FEED_TYPES, // Metadata comes from any relay
filter = JsonFilter( filter = JsonFilter(
@@ -66,8 +72,10 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
} }
} }
fun createLastMessageOfEachChannelFilter(): List<TypedFilter> { fun createLastMessageOfEachChannelFilter(): List<TypedFilter>? {
return account.followingChannels.map { val followingEvents = account.selectedChatsFollowList()
return followingEvents.map {
TypedFilter( TypedFilter(
types = setOf(FeedType.PUBLIC_CHATS), types = setOf(FeedType.PUBLIC_CHATS),
filter = JsonFilter( filter = JsonFilter(
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent 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.GenericRepostEvent
import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent 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 -> val followAccountChannel = requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList, relayUrl, time) latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList, relayUrl, time)
} }
override fun updateChannelFilters() { override fun updateChannelFilters() {
followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowTagsFilter()).ifEmpty { null } followAccountChannel.typedFilters = listOfNotNull(createFollowAccountsFilter(), createFollowCommunitiesFilter(), createFollowTagsFilter()).ifEmpty { null }
} }
} }
@@ -39,6 +39,10 @@ class ContactListEvent(
unverifiedFollowTagSet().map { it.lowercase() }.toSet() unverifiedFollowTagSet().map { it.lowercase() }.toSet()
} }
val verifiedFollowCommunitySet: Set<String> by lazy {
unverifiedFollowAddressSet().toSet()
}
val verifiedFollowKeySetAndMe: Set<HexKey> by lazy { val verifiedFollowKeySetAndMe: Set<HexKey> by lazy {
verifiedFollowKeySet + pubKey verifiedFollowKeySet + pubKey
} }
@@ -46,6 +50,8 @@ class ContactListEvent(
fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) } fun unverifiedFollowKeySet() = tags.filter { it[0] == "p" }.mapNotNull { it.getOrNull(1) }
fun unverifiedFollowTagSet() = tags.filter { it[0] == "t" }.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 { fun follows() = tags.filter { it[0] == "p" }.mapNotNull {
try { try {
Contact(decodePublicKey(it[1]).toHexKey(), it.getOrNull(2)) Contact(decodePublicKey(it[1]).toHexKey(), it.getOrNull(2))
@@ -73,14 +79,22 @@ class ContactListEvent(
companion object { companion object {
const val kind = 3 const val kind = 3
fun create(follows: List<Contact>, followTags: List<String>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { fun createFromScratch(
followUsers: List<Contact>,
followTags: List<String>,
followCommunities: List<ATag>,
followEvents: List<String>,
relayUse: Map<String, ReadWrite>?,
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): ContactListEvent {
val content = if (relayUse != null) { val content = if (relayUse != null) {
gson.toJson(relayUse) gson.toJson(relayUse)
} else { } else {
"" ""
} }
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = follows.map { val tags = followUsers.map {
if (it.relayUri != null) { if (it.relayUri != null) {
listOf("p", it.pubKeyHex, it.relayUri) listOf("p", it.pubKeyHex, it.relayUri)
} else { } else {
@@ -88,8 +102,129 @@ class ContactListEvent(
} }
} + followTags.map { } + followTags.map {
listOf("t", it) 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<String, ReadWrite>?, 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<List<String>>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
@@ -44,10 +44,10 @@ open class Event(
fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" } fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" }
fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] } override 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 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]) } 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) 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 aTagValue = it[1]
val relay = it.getOrNull(2) 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 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 isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex }
override fun isTaggedAddressableNotes(idHexes: Set<String>) = 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 isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags } override fun isTaggedHashes(hashtags: Set<String>) = tags.any { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }
override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1) override fun firstIsTaggedHashes(hashtags: Set<String>) = tags.firstOrNull { it.size > 1 && it[0] == "t" && it[1].lowercase() in hashtags }?.getOrNull(1)
@@ -27,7 +27,11 @@ interface EventInterface {
fun hasValidSignature(): Boolean fun hasValidSignature(): Boolean
fun isTaggedUser(idHex: String): Boolean fun isTaggedUser(idHex: String): Boolean
fun isTaggedEvent(idHex: String): Boolean
fun isTaggedAddressableNote(idHex: String): Boolean fun isTaggedAddressableNote(idHex: String): Boolean
fun isTaggedAddressableNotes(idHexes: Set<String>): Boolean
fun isTaggedHash(hashtag: String): Boolean fun isTaggedHash(hashtag: String): Boolean
fun isTaggedHashes(hashtags: Set<String>): Boolean fun isTaggedHashes(hashtags: Set<String>): Boolean
@@ -46,6 +50,11 @@ interface EventInterface {
fun isSensitive(): Boolean fun isSensitive(): Boolean
fun zapraiserAmount(): Long? fun zapraiserAmount(): Long?
fun taggedAddresses(): List<ATag>
fun taggedUsers(): List<HexKey>
fun taggedEvents(): List<HexKey>
fun taggedUrls(): List<String>
fun taggedEmojis(): List<EmojiUrl> fun taggedEmojis(): List<EmojiUrl>
fun matchTag1With(text: String): Boolean fun matchTag1With(text: String): Boolean
} }
@@ -54,7 +54,13 @@ abstract class GeneralListEvent(
return privateTagsCache return privateTagsCache
} }
fun privateTagsOrEmpty(privKey: ByteArray): List<List<String>> {
return privateTags(privKey) ?: emptyList()
}
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } 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 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 { fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull {
val aTagValue = it.getOrNull(1) val aTagValue = it.getOrNull(1)
@@ -90,5 +96,16 @@ abstract class GeneralListEvent(
pubKey pubKey
) )
} }
fun encryptTags(
privateTags: List<List<String>>? = null,
privateKey: ByteArray
): String {
return Utils.encrypt(
msg = gson.toJson(privateTags),
privateKey = privateKey,
pubKey = Utils.pubkeyCreate(privateKey)
)
}
} }
} }
@@ -14,8 +14,6 @@ interface LnZapEventInterface : EventInterface {
fun zappedRequestAuthor(): String? fun zappedRequestAuthor(): String?
fun taggedAddresses(): List<ATag>
fun amount(): BigDecimal? fun amount(): BigDecimal?
fun containedPost(): Event? fun containedPost(): Event?
@@ -4,6 +4,9 @@ import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey 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 import nostr.postr.Utils
@Immutable @Immutable
@@ -15,42 +18,133 @@ class PeopleListEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
var publicAndPrivateUserCache: ImmutableSet<HexKey>? = null
fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet<HexKey> {
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 { companion object {
const val kind = 30000 const val kind = 30000
const val blockList = "mute"
fun create( fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
name: String = "", return if (isPrivate) {
create(
events: List<String>? = null, content = encryptTags(listOf(listOf("p", pubKeyHex)), privateKey),
users: List<String>? = null, tags = listOf(listOf("d", name)),
addresses: List<ATag>? = null, privateKey = privateKey,
createdAt = createdAt
privEvents: List<String>? = null, )
privUsers: List<String>? = null, } else {
privAddresses: List<ATag>? = null, create(
content = "",
privateKey: ByteArray, tags = listOf(listOf("d", name), listOf("p", pubKeyHex)),
createdAt: Long = TimeUtils.now() privateKey = privateKey,
): PeopleListEvent { createdAt = createdAt
val pubKey = Utils.pubkeyCreate(privateKey) )
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
val tags = mutableListOf<List<String>>()
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()))
} }
}
val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content) fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List<String>, 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<List<String>>, 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) 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())
} }
} }
} }
@@ -32,7 +32,9 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
?.lastOrNull { it.event != null } ?.lastOrNull { it.event != null }
} }
val publicChannels = account.followingChannels().map { it -> val publicChannels = account.selectedChatsFollowList().mapNotNull {
LocalCache.getChannelIfExists(it)
}.mapNotNull { it ->
it.notes.values it.notes.values
.filter { account.isAcceptable(it) && it.event != null } .filter { account.isAcceptable(it) && it.event != null }
.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) .sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
@@ -40,7 +42,6 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
} }
return (privateMessages + publicChannels) return (privateMessages + publicChannels)
.filterNotNull()
.sortedWith(compareBy({ it.createdAt() }, { it.idHex })) .sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
.reversed() .reversed()
} }
@@ -106,7 +107,7 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
} }
private fun filterRelevantPublicMessages(newItems: Set<Note>, account: Account): MutableMap<String, Note> { private fun filterRelevantPublicMessages(newItems: Set<Note>, account: Account): MutableMap<String, Note> {
val followingChannels = account.followingChannels val followingChannels = account.userProfile().latestContactList?.taggedEvents()?.toSet() ?: emptySet()
val newRelevantPublicMessages = mutableMapOf<String, Note>() val newRelevantPublicMessages = mutableMapOf<String, Note>()
newItems.filter { it.event is ChannelMessageEvent }.forEach { newNote -> newItems.filter { it.event is ChannelMessageEvent }.forEach { newNote ->
newNote.channelHex()?.let { channelHex -> newNote.channelHex()?.let { channelHex ->
@@ -13,6 +13,10 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
} }
override fun showHiddenKey(): Boolean {
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
}
override fun feed(): List<Note> { override fun feed(): List<Note> {
val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) } val allChannelNotes = LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
@@ -28,6 +32,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> { protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = TimeUtils.now() val now = TimeUtils.now()
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey()
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
@@ -36,15 +41,15 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val anyOtherChannelEvent = collection val anyOtherChannelEvent = collection
.asSequence() .asSequence()
.filter { it.event is IsInPublicChatChannel } .filter { it.event is IsInPublicChatChannel }
.mapNotNull { (it.event as? ChannelMessageEvent)?.channel() } .mapNotNull { (it.event as? IsInPublicChatChannel)?.channel() }
.mapNotNull { LocalCache.checkGetOrCreateNote(it) } .mapNotNull { LocalCache.checkGetOrCreateNote(it) }
.toSet() .toSet()
val activities = (createEvents + anyOtherChannelEvent) val activities = (createEvents + anyOtherChannelEvent)
.asSequence() .asSequence()
.filter { it.event is ChannelCreateEvent } // .filter { it.event is ChannelCreateEvent } // Event heads might not be loaded yet.
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true } .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 } .filter { (it.createdAt() ?: 0) <= now }
.toSet() .toSet()
@@ -13,6 +13,10 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
} }
override fun showHiddenKey(): Boolean {
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
}
override fun feed(): List<Note> { override fun feed(): List<Note> {
val allNotes = LocalCache.addressables.values val allNotes = LocalCache.addressables.values
@@ -28,15 +32,24 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> { protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = TimeUtils.now() val now = TimeUtils.now()
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey()
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(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() .asSequence()
.filter { it.event is CommunityDefinitionEvent } .filter { it.event is CommunityDefinitionEvent }
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true } .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 } .filter { (it.createdAt() ?: 0) <= now }
.toSet() .toSet()
@@ -16,6 +16,10 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
} }
override fun showHiddenKey(): Boolean {
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
}
override fun feed(): List<Note> { override fun feed(): List<Note> {
val allChannelNotes = val allChannelNotes =
LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) } LocalCache.channels.values.mapNotNull { LocalCache.getNoteIfExists(it.idHex) }
@@ -33,6 +37,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> { protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = TimeUtils.now() val now = TimeUtils.now()
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey()
val followingKeySet = val followingKeySet =
account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
@@ -47,7 +52,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
followingTagSet followingTagSet
) == true ) == 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 } .filter { (it.createdAt() ?: 0) <= now }
.toSet() .toSet()
@@ -24,6 +24,7 @@ abstract class FeedFilter<T> {
* Returns a string that serves as the key to invalidate the list if it changes. * Returns a string that serves as the key to invalidate the list if it changes.
*/ */
abstract fun feedKey(): String abstract fun feedKey(): String
open fun showHiddenKey(): Boolean = false
abstract fun feed(): List<T> abstract fun feed(): List<T>
} }
@@ -9,8 +9,28 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
return account.userProfile().pubkeyHex return account.userProfile().pubkeyHex
} }
override fun showHiddenKey(): Boolean {
return true
}
override fun feed(): List<User> { override fun feed(): List<User> {
return (account.hiddenUsers + account.transientHiddenUsers) return account.getBlockList()
.map { LocalCache.getOrCreateUser(it) } ?.publicAndPrivateUsers(account.loggedIn.privKey)
?.map { LocalCache.getOrCreateUser(it) }
?: emptyList()
}
}
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex
}
override fun showHiddenKey(): Boolean {
return true
}
override fun feed(): List<User> {
return (account.transientHiddenUsers).map { LocalCache.getOrCreateUser(it) }
} }
} }
@@ -7,6 +7,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TimeUtils import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent 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.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
@@ -16,6 +17,10 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList
} }
override fun showHiddenKey(): Boolean {
return account.defaultHomeFollowList == PeopleListEvent.blockList
}
override fun feed(): List<Note> { override fun feed(): List<Note> {
return sort(innerApplyFilter(LocalCache.notes.values)) return sort(innerApplyFilter(LocalCache.notes.values))
} }
@@ -26,6 +31,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> { private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey()
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
@@ -38,7 +44,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
(it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent || it.event is LiveActivitiesChatMessageEvent) && (it.event is TextNoteEvent || it.event is PollNoteEvent || it.event is ChannelMessageEvent || it.event is LiveActivitiesChatMessageEvent) &&
(isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) ?: false) && (isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) ?: false) &&
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable // && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
it.author?.let { !account.isHidden(it) } ?: true && (isHiddenList || it.author?.let { !account.isHidden(it) } ?: true) &&
((it.event?.createdAt() ?: 0) < now) && ((it.event?.createdAt() ?: 0) < now) &&
!it.isNewThread() !it.isNewThread()
} }
@@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
import com.vitorpamplona.amethyst.service.model.HighlightEvent import com.vitorpamplona.amethyst.service.model.HighlightEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
@@ -19,6 +20,10 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList return account.userProfile().pubkeyHex + "-" + account.defaultHomeFollowList
} }
override fun showHiddenKey(): Boolean {
return account.defaultHomeFollowList == PeopleListEvent.blockList
}
override fun feed(): List<Note> { override fun feed(): List<Note> {
val notes = innerApplyFilter(LocalCache.notes.values) val notes = innerApplyFilter(LocalCache.notes.values)
val longFormNotes = innerApplyFilter(LocalCache.addressables.values) val longFormNotes = innerApplyFilter(LocalCache.addressables.values)
@@ -32,9 +37,11 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> { private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey()
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(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 oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future.
val oneHr = 60 * 60 val oneHr = 60 * 60
@@ -44,9 +51,9 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
.filter { it -> .filter { it ->
val noteEvent = it.event 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) && (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 // && 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.event?.createdAt() ?: 0) < oneMinuteInTheFuture) &&
it.isNewThread() && it.isNewThread() &&
( (
@@ -12,6 +12,10 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
return account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList return account.userProfile().pubkeyHex + "-" + account.defaultNotificationFollowList
} }
override fun showHiddenKey(): Boolean {
return account.defaultNotificationFollowList == PeopleListEvent.blockList
}
override fun feed(): List<Note> { override fun feed(): List<Note> {
return sort(innerApplyFilter(LocalCache.notes.values)) return sort(innerApplyFilter(LocalCache.notes.values))
} }
@@ -22,6 +26,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> { private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val isGlobal = account.defaultNotificationFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultNotificationFollowList == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey()
val followingKeySet = account.selectedUsersFollowList(account.defaultNotificationFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultNotificationFollowList) ?: emptySet()
@@ -37,7 +42,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
it.author !== loggedInUser && it.author !== loggedInUser &&
(isGlobal || it.author?.pubkeyHex in followingKeySet) && (isGlobal || it.author?.pubkeyHex in followingKeySet) &&
it.event?.isTaggedUser(loggedInUserHex) ?: false && 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) tagsAnEventByUser(it, loggedInUserHex)
}.toSet() }.toSet()
} }
@@ -12,6 +12,10 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
return account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList return account.userProfile().pubkeyHex + "-" + account.defaultStoriesFollowList
} }
override fun showHiddenKey(): Boolean {
return account.defaultStoriesFollowList == PeopleListEvent.blockList
}
override fun feed(): List<Note> { override fun feed(): List<Note> {
val notes = innerApplyFilter(LocalCache.notes.values) val notes = innerApplyFilter(LocalCache.notes.values)
@@ -25,6 +29,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> { private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = TimeUtils.now() val now = TimeUtils.now()
val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS
val isHiddenList = account.defaultStoriesFollowList == PeopleListEvent.blockList
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultStoriesFollowList) ?: emptySet()
@@ -33,7 +38,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
.asSequence() .asSequence()
.filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent } .filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent }
.filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) } .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 } .filter { it.createdAt()!! <= now }
.toSet() .toSet()
} }
@@ -43,7 +43,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @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 noteState by likeSetCard.note.live().metadata.observeAsState()
val note = noteState?.note val note = noteState?.note
@@ -158,6 +158,7 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
baseNote = it, baseNote = it,
routeForLastRead = null, routeForLastRead = null,
isBoostedNote = true, isBoostedNote = true,
showHidden = showHidden,
parentBackgroundColor = backgroundColor, parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -87,6 +87,7 @@ fun ChannelCardCompose(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
forceEventKind: Int?, forceEventKind: Int?,
showHidden: Boolean = false,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
@@ -114,6 +115,7 @@ fun ChannelCardCompose(
routeForLastRead, routeForLastRead,
modifier, modifier,
parentBackgroundColor, parentBackgroundColor,
showHidden,
accountViewModel, accountViewModel,
nav nav
) )
@@ -128,24 +130,47 @@ fun CheckHiddenChannelCardCompose(
routeForLastRead: String? = null, routeForLastRead: String? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
showHidden: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val isHidden by accountViewModel.accountLiveData.map { if (showHidden) {
accountViewModel.isNoteHidden(note) var state by remember {
}.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note)) mutableStateOf(
NoteComposeReportState(
Crossfade(targetState = isHidden) { isAcceptable = true,
if (!it) { canPreview = true,
LoadedChannelCardCompose( relevantReports = persistentSetOf()
note, )
routeForLastRead,
modifier,
parentBackgroundColor,
accountViewModel,
nav
) )
} }
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) { fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val channelUpdates by channel.live.observeAsState() val channelUpdates by channel.live.observeAsState()
val name = remember(channelUpdates) { channel.toBestDisplayName() } val name = remember(channelUpdates) { channelUpdates?.channel?.toBestDisplayName() ?: "" }
val description = remember(channelUpdates) { channel.summary() } val description = remember(channelUpdates) { channelUpdates?.channel?.summary() }
val cover by remember(channelUpdates) { val cover by remember(channelUpdates) {
derivedStateOf { derivedStateOf {
channel.profilePicture()?.ifBlank { null } channelUpdates?.channel?.profilePicture()?.ifBlank { null }
} }
} }
@@ -35,7 +35,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @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 baseNote = remember { messageSetCard.note }
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -102,6 +102,7 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
routeForLastRead = null, routeForLastRead = null,
isBoostedNote = true, isBoostedNote = true,
addMarginTop = false, addMarginTop = false,
showHidden = showHidden,
parentBackgroundColor = backgroundColor, parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -32,7 +32,6 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -86,7 +85,7 @@ import kotlin.time.measureTimedValue
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@Composable @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 baseNote = remember { multiSetCard.note }
val popupExpanded = remember { mutableStateOf(false) } val popupExpanded = remember { mutableStateOf(false) }
@@ -154,6 +153,7 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
routeForLastRead = null, routeForLastRead = null,
modifier = remember { Modifier.padding(top = 5.dp) }, modifier = remember { Modifier.padding(top = 5.dp) },
isBoostedNote = true, isBoostedNote = true,
showHidden = showHidden,
parentBackgroundColor = backgroundColor, parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -562,7 +562,7 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
} }
} }
WatchFollows(author.pubkeyHex, accountViewModel) { isFollowing -> WatchUserFollows(author.pubkeyHex, accountViewModel) { isFollowing ->
Crossfade(targetState = isFollowing) { Crossfade(targetState = isFollowing) {
if (it) { if (it) {
Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) { Box(modifier = Size35Modifier, contentAlignment = Alignment.TopEnd) {
@@ -214,6 +214,7 @@ fun NoteCompose(
unPackReply: Boolean = true, unPackReply: Boolean = true,
makeItShort: Boolean = false, makeItShort: Boolean = false,
addMarginTop: Boolean = true, addMarginTop: Boolean = true,
showHidden: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -245,6 +246,7 @@ fun NoteCompose(
unPackReply, unPackReply,
makeItShort, makeItShort,
addMarginTop, addMarginTop,
showHidden,
parentBackgroundColor, parentBackgroundColor,
accountViewModel, accountViewModel,
nav nav
@@ -263,30 +265,59 @@ fun CheckHiddenNoteCompose(
unPackReply: Boolean = true, unPackReply: Boolean = true,
makeItShort: Boolean = false, makeItShort: Boolean = false,
addMarginTop: Boolean = true, addMarginTop: Boolean = true,
showHidden: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val isHidden by accountViewModel.accountLiveData.map { if (showHidden) {
accountViewModel.isNoteHidden(note) // Ignores reports as well
}.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note)) var state by remember {
mutableStateOf(
Crossfade(targetState = isHidden) { NoteComposeReportState(
if (!it) { isAcceptable = true,
LoadedNoteCompose( canPreview = true,
note, relevantReports = persistentSetOf()
routeForLastRead, )
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
parentBackgroundColor,
accountViewModel,
nav
) )
} }
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, accountViewModel = accountViewModel,
nav = nav nav = nav
) )
is CommunityDefinitionEvent -> CommunityHeader( is CommunityDefinitionEvent -> (baseNote as? AddressableNote)?.let {
baseNote = baseNote, CommunityHeader(
showBottomDiviser = true, baseNote = it,
sendToCommunity = true, showBottomDiviser = true,
accountViewModel = accountViewModel, sendToCommunity = true,
nav = nav accountViewModel = accountViewModel,
) nav = nav
)
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote) is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is FileHeaderEvent -> FileHeaderDisplay(baseNote, accountViewModel) is FileHeaderEvent -> FileHeaderDisplay(baseNote, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, accountViewModel) is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, accountViewModel)
@@ -471,14 +504,14 @@ fun NormalNote(
@Composable @Composable
fun CommunityHeader( fun CommunityHeader(
baseNote: Note, baseNote: AddressableNote,
showBottomDiviser: Boolean, showBottomDiviser: Boolean,
sendToCommunity: Boolean, sendToCommunity: Boolean,
modifier: Modifier = StdPadding, modifier: Modifier = StdPadding,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
var expanded = remember { mutableStateOf(false) } val expanded = remember { mutableStateOf(false) }
Column( Column(
modifier = modifier.clickable { modifier = modifier.clickable {
@@ -506,7 +539,7 @@ fun CommunityHeader(
} }
@Composable @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 noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
@@ -648,7 +681,7 @@ fun LongCommunityHeader(baseNote: Note, accountViewModel: AccountViewModel, nav:
} }
@Composable @Composable
fun ShortCommunityHeader(baseNote: Note, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteState by baseNote.live().metadata.observeAsState() val noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
@@ -713,44 +746,45 @@ fun ShortCommunityHeader(baseNote: Note, expanded: MutableState<Boolean>, accoun
@Composable @Composable
private fun ShortCommunityActionOptions( private fun ShortCommunityActionOptions(
note: Note, note: AddressableNote,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit 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) Spacer(modifier = StdHorzSpacer)
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav) LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel) ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
if (!isFollowing) { WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
Spacer(modifier = StdHorzSpacer) if (!isFollowing) {
JoinCommunityButton(accountViewModel, note, nav) 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 @Composable
private fun LongCommunityActionOptions( private fun LongCommunityActionOptions(
note: Note, note: AddressableNote,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState() WatchAddressableNoteFollows(note, accountViewModel) { isFollowing ->
val isFollowing by remember(accountState) { if (isFollowing) {
derivedStateOf { LeaveCommunityButton(accountViewModel, note, nav)
accountState?.account?.followingCommunities?.contains(note.idHex) ?: false
} }
} }
if (isFollowing) {
LeaveCommunityButton(accountViewModel, note, nav)
}
} }
@Composable @Composable
@@ -363,6 +363,20 @@ private fun WatchReactionsZapsBoostsAndDisplayIfExists(baseNote: Note, content:
} }
} }
fun <T, K, R> LiveData<T>.combineWith(
liveData1: LiveData<K>,
block: (T?, K?) -> R
): LiveData<R> {
val result = MediatorLiveData<R>()
result.addSource(this) {
result.value = block(this.value, liveData1.value)
}
result.addSource(liveData1) {
result.value = block(this.value, liveData1.value)
}
return result
}
fun <T, K, P, R> LiveData<T>.combineWith( fun <T, K, P, R> LiveData<T>.combineWith(
liveData1: LiveData<K>, liveData1: LiveData<K>,
liveData2: LiveData<P>, liveData2: LiveData<P>,
@@ -264,7 +264,7 @@ fun PictureAndFollowingMark(
@Composable @Composable
fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) { fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
WatchFollows(userHex, accountViewModel) { newFollowingState -> WatchUserFollows(userHex, accountViewModel) { newFollowingState ->
Crossfade(targetState = newFollowingState) { following -> Crossfade(targetState = newFollowingState) { following ->
if (following) { if (following) {
Box(contentAlignment = Alignment.TopEnd) { Box(contentAlignment = Alignment.TopEnd) {
@@ -276,7 +276,7 @@ fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewMod
} }
@Composable @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 { val showFollowingMark by accountViewModel.userFollows.map {
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex) it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
}.distinctUntilChanged().observeAsState( }.distinctUntilChanged().observeAsState(
@@ -174,7 +174,9 @@ fun UserActionOptions(
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val accountState by accountViewModel.accountLiveData.observeAsState() 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 { derivedStateOf {
accountState?.account?.isHidden(baseAuthor) ?: false accountState?.account?.isHidden(baseAuthor) ?: false
} }
@@ -141,9 +141,10 @@ fun RenderCardFeed(
FeedLoaded( FeedLoaded(
state = state, state = state,
listState = listState, listState = listState,
routeForLastRead = routeForLastRead,
showHidden = viewModel.showHidden(),
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav, nav = nav
routeForLastRead = routeForLastRead
) )
} }
CardFeedState.Loading -> { CardFeedState.Loading -> {
@@ -158,9 +159,10 @@ fun RenderCardFeed(
private fun FeedLoaded( private fun FeedLoaded(
state: CardFeedState.Loaded, state: CardFeedState.Loaded,
listState: LazyListState, listState: LazyListState,
showHidden: Boolean,
routeForLastRead: String,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit
routeForLastRead: String
) { ) {
LazyColumn( LazyColumn(
modifier = remember { Modifier.fillMaxSize() }, modifier = remember { Modifier.fillMaxSize() },
@@ -179,7 +181,7 @@ private fun FeedLoaded(
} }
Row(defaultModifier) { Row(defaultModifier) {
RenderCardItem(item, accountViewModel, nav, routeForLastRead) RenderCardItem(item, routeForLastRead, showHidden, accountViewModel, nav)
} }
} }
} }
@@ -188,15 +190,17 @@ private fun FeedLoaded(
@Composable @Composable
private fun RenderCardItem( private fun RenderCardItem(
item: Card, item: Card,
routeForLastRead: String,
showHidden: Boolean,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit
routeForLastRead: String
) { ) {
when (item) { when (item) {
is NoteCard -> NoteCompose( is NoteCard -> NoteCompose(
item, item,
isBoostedNote = false, isBoostedNote = false,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
showHidden = showHidden,
nav = nav, nav = nav,
routeForLastRead = routeForLastRead routeForLastRead = routeForLastRead
) )
@@ -212,6 +216,7 @@ private fun RenderCardItem(
is MultiSetCard -> MultiSetCompose( is MultiSetCard -> MultiSetCompose(
item, item,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
showHidden = showHidden,
nav = nav, nav = nav,
routeForLastRead = routeForLastRead routeForLastRead = routeForLastRead
) )
@@ -219,6 +224,7 @@ private fun RenderCardItem(
is BadgeCard -> BadgeCompose( is BadgeCard -> BadgeCompose(
item, item,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
showHidden = showHidden,
nav = nav, nav = nav,
routeForLastRead = routeForLastRead routeForLastRead = routeForLastRead
) )
@@ -226,6 +232,7 @@ private fun RenderCardItem(
is MessageSetCard -> MessageSetCompose( is MessageSetCard -> MessageSetCompose(
messageSetCard = item, messageSetCard = item,
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
showHidden = showHidden,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
) )
@@ -242,6 +249,7 @@ fun NoteCompose(
unPackReply: Boolean = true, unPackReply: Boolean = true,
makeItShort: Boolean = false, makeItShort: Boolean = false,
addMarginTop: Boolean = true, addMarginTop: Boolean = true,
showHidden: Boolean = false,
parentBackgroundColor: MutableState<Color>? = null, parentBackgroundColor: MutableState<Color>? = null,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
@@ -259,6 +267,7 @@ fun NoteCompose(
unPackReply = unPackReply, unPackReply = unPackReply,
makeItShort = makeItShort, makeItShort = makeItShort,
addMarginTop = addMarginTop, addMarginTop = addMarginTop,
showHidden = showHidden,
parentBackgroundColor = parentBackgroundColor, parentBackgroundColor = parentBackgroundColor,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
@@ -71,6 +71,10 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
scrolltoTopPending = false scrolltoTopPending = false
} }
fun showHidden(): Boolean {
return localFilter.showHiddenKey()
}
private var lastAccount: Account? = null private var lastAccount: Account? = null
private var lastNotes: Set<Note>? = null private var lastNotes: Set<Note>? = null
@@ -151,6 +151,7 @@ private fun RenderFeed(
state, state,
listState, listState,
routeForLastRead, routeForLastRead,
viewModel.showHidden(),
accountViewModel, accountViewModel,
nav nav
) )
@@ -184,6 +185,7 @@ private fun FeedLoaded(
state: FeedState.Loaded, state: FeedState.Loaded,
listState: LazyListState, listState: LazyListState,
routeForLastRead: String?, routeForLastRead: String?,
showHidden: Boolean = false,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
@@ -210,6 +212,7 @@ private fun FeedLoaded(
routeForLastRead = routeForLastRead, routeForLastRead = routeForLastRead,
modifier = baseModifier, modifier = baseModifier,
isBoostedNote = false, isBoostedNote = false,
showHidden = showHidden,
accountViewModel = accountViewModel, accountViewModel = accountViewModel,
nav = nav nav = nav
) )
@@ -217,6 +217,10 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
scrolltoTopPending = false scrolltoTopPending = false
} }
fun showHidden(): Boolean {
return localFilter.showHiddenKey()
}
private fun refresh() { private fun refresh() {
val scope = CoroutineScope(Job() + Dispatchers.Default) val scope = CoroutineScope(Job() + Dispatchers.Default)
scope.launch { scope.launch {
@@ -13,6 +13,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter 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.UserProfileFollowersFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
import kotlinx.collections.immutable.ImmutableList 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 <NostrSpammerAccountsFeedViewModel : ViewModel> create(modelClass: Class<NostrSpammerAccountsFeedViewModel>): NostrSpammerAccountsFeedViewModel {
return NostrSpammerAccountsFeedViewModel(account) as NostrSpammerAccountsFeedViewModel
}
}
}
@Stable @Stable
open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), InvalidatableViewModel { open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), InvalidatableViewModel {
private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading) private val _feedContent = MutableStateFlow<UserFeedState>(UserFeedState.Loading)
@@ -61,6 +70,10 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
} }
} }
fun showHidden(): Boolean {
return dataSource.showHiddenKey()
}
private fun refreshSuspended() { private fun refreshSuspended() {
checkNotInMainThread() checkNotInMainThread()
@@ -80,6 +80,7 @@ import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.AddressableNote
import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LiveActivitiesChannel
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
@@ -863,13 +864,6 @@ private fun ShortChannelActionOptions(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit 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) { LoadNote(baseNoteHex = channel.idHex) {
it?.let { it?.let {
var popupExpanded by remember { mutableStateOf(false) } var popupExpanded by remember { mutableStateOf(false) }
@@ -882,11 +876,28 @@ private fun ShortChannelActionOptions(
} }
} }
if (!isFollowing) { WatchChannelFollows(channel, accountViewModel) { isFollowing ->
JoinChatButton(accountViewModel, channel, nav) 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 @Composable
private fun LongChannelActionOptions( private fun LongChannelActionOptions(
channel: PublicChatChannel, 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) { if (isMe) {
EditButton(accountViewModel, channel) EditButton(accountViewModel, channel)
} }
if (isFollowing) { WatchChannelFollows(channel, accountViewModel) { isFollowing ->
LeaveChatButton(accountViewModel, channel, nav) if (isFollowing) {
LeaveChatButton(accountViewModel, channel, nav)
}
} }
} }
@@ -1074,10 +1080,14 @@ private fun EditButton(accountViewModel: AccountViewModel, channel: PublicChatCh
@Composable @Composable
fun JoinChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) { fun JoinChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
val scope = rememberCoroutineScope()
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
accountViewModel.account.joinChannel(channel.idHex) scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(channel)
}
}, },
shape = ButtonBorder, shape = ButtonBorder,
colors = ButtonDefaults colors = ButtonDefaults
@@ -1092,10 +1102,14 @@ fun JoinChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (S
@Composable @Composable
fun LeaveChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) { fun LeaveChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (String) -> Unit) {
val scope = rememberCoroutineScope()
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
accountViewModel.account.leaveChannel(channel.idHex) scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollow(channel)
}
}, },
shape = ButtonBorder, shape = ButtonBorder,
colors = ButtonDefaults colors = ButtonDefaults
@@ -1109,11 +1123,15 @@ fun LeaveChatButton(accountViewModel: AccountViewModel, channel: Channel, nav: (
} }
@Composable @Composable
fun JoinCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (String) -> Unit) { fun JoinCommunityButton(accountViewModel: AccountViewModel, note: AddressableNote, nav: (String) -> Unit) {
val scope = rememberCoroutineScope()
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
accountViewModel.account.joinCommunity(note.idHex) scope.launch(Dispatchers.IO) {
accountViewModel.account.follow(note)
}
}, },
shape = ButtonBorder, shape = ButtonBorder,
colors = ButtonDefaults colors = ButtonDefaults
@@ -1127,11 +1145,15 @@ fun JoinCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (St
} }
@Composable @Composable
fun LeaveCommunityButton(accountViewModel: AccountViewModel, note: Note, nav: (String) -> Unit) { fun LeaveCommunityButton(accountViewModel: AccountViewModel, note: AddressableNote, nav: (String) -> Unit) {
val scope = rememberCoroutineScope()
Button( Button(
modifier = Modifier.padding(horizontal = 3.dp), modifier = Modifier.padding(horizontal = 3.dp),
onClick = { onClick = {
accountViewModel.account.leaveCommunity((note.idHex)) scope.launch(Dispatchers.IO) {
accountViewModel.account.unfollow(note)
}
}, },
shape = ButtonBorder, shape = ButtonBorder,
colors = ButtonDefaults colors = ButtonDefaults
@@ -32,23 +32,30 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel 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.RefreshingFeedUserFeedView
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) { fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val feedViewModel: NostrHiddenAccountsFeedViewModel = viewModel( val hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel = viewModel(
factory = NostrHiddenAccountsFeedViewModel.Factory(accountViewModel.account) 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun HiddenUsersScreen( fun HiddenUsersScreen(
feedViewModel: NostrHiddenAccountsFeedViewModel, hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel,
spammerFeedViewModel: NostrSpammerAccountsFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
@@ -58,10 +65,8 @@ fun HiddenUsersScreen(
val observer = LifecycleEventObserver { _, event -> val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Hidden Users Start") println("Hidden Users Start")
feedViewModel.invalidateData() hiddenFeedViewModel.invalidateData()
} spammerFeedViewModel.invalidateData()
if (event == Lifecycle.Event.ON_PAUSE) {
println("Hidden Users Stop")
} }
} }
@@ -116,10 +121,18 @@ fun HiddenUsersScreen(
Text(text = stringResource(R.string.blocked_users)) 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) { when (page) {
0 -> RefreshingUserFeedView(feedViewModel, accountViewModel, nav) 0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel, nav)
1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel, nav)
} }
} }
} }
@@ -128,22 +141,23 @@ fun HiddenUsersScreen(
@Composable @Composable
fun RefreshingUserFeedView( fun RefreshingUserFeedView(
feedViewModel: NostrHiddenAccountsFeedViewModel, feedViewModel: UserFeedViewModel,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
WatchAccount(feedViewModel, accountViewModel) WatchAccountAndBlockList(feedViewModel, accountViewModel)
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav) RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav)
} }
@Composable @Composable
fun WatchAccount( fun WatchAccountAndBlockList(
feedViewModel: NostrHiddenAccountsFeedViewModel, feedViewModel: UserFeedViewModel,
accountViewModel: AccountViewModel accountViewModel: AccountViewModel
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val blockListState by accountViewModel.account.getBlockListNote().live().metadata.observeAsState()
LaunchedEffect(accountViewModel, accountState) { LaunchedEffect(accountViewModel, accountState, blockListState) {
feedViewModel.invalidateData() feedViewModel.invalidateData()
} }
} }
@@ -701,45 +701,48 @@ private fun ProfileHeader(
private fun ProfileActions( private fun ProfileActions(
baseUser: User, baseUser: User,
accountViewModel: AccountViewModel 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 scope = rememberCoroutineScope()
val context = LocalContext.current val context = LocalContext.current
val accountLocalUserState by accountViewModel.accountLiveData.observeAsState() val isLoggedInFollowingUser by accountViewModel.account.userProfile().live().follows.map {
val account = remember(accountLocalUserState) { accountLocalUserState?.account } ?: return it.user.isFollowing(baseUser)
}.distinctUntilChanged().observeAsState(initial = accountViewModel.account.isFollowing(baseUser))
val accountUserState by accountViewModel.account.userProfile().live().follows.observeAsState() val isUserFollowingLoggedIn by baseUser.live().follows.map {
val baseUserState by baseUser.live().follows.observeAsState() it.user.isFollowing(accountViewModel.account.userProfile())
}.distinctUntilChanged().observeAsState(initial = baseUser.isFollowing(accountViewModel.account.userProfile()))
val accountUser = remember(accountUserState) { accountUserState?.user } ?: return if (isLoggedInFollowingUser) {
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) {
UnfollowButton { UnfollowButton {
if (!accountViewModel.isWriteable()) { if (!accountViewModel.isWriteable()) {
scope.launch { scope.launch {
@@ -753,7 +756,7 @@ private fun ProfileActions(
} }
} else { } else {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
account.unfollow(baseUser) accountViewModel.account.unfollow(baseUser)
} }
} }
} }
@@ -772,7 +775,7 @@ private fun ProfileActions(
} }
} else { } else {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
account.follow(baseUser) accountViewModel.account.follow(baseUser)
} }
} }
} }
@@ -790,7 +793,7 @@ private fun ProfileActions(
} }
} else { } else {
scope.launch(Dispatchers.IO) { 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 @Composable
private fun DrawAdditionalInfo( private fun DrawAdditionalInfo(
baseUser: User, baseUser: User,
@@ -1638,6 +1650,7 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account!! val account = accountState?.account!!
val blockList by accountViewModel.account.getBlockListNote().live().metadata.observeAsState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
+2
View File
@@ -494,4 +494,6 @@
<string name="automatically_play_videos">Automatically play videos</string> <string name="automatically_play_videos">Automatically play videos</string>
<string name="automatically_show_url_preview">Automatically show url preview</string> <string name="automatically_show_url_preview">Automatically show url preview</string>
<string name="load_image">Load Image</string> <string name="load_image">Load Image</string>
<string name="spamming_users">Spammers</string>
</resources> </resources>