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