block users using amber

This commit is contained in:
greenart7c3
2023-08-30 11:54:36 -03:00
parent bc8fa00608
commit 7a68cf867e
6 changed files with 263 additions and 12 deletions
@@ -105,12 +105,42 @@ class Account(
val liveHiddenUsers: LiveData<LiveHiddenUsers> by lazy { val liveHiddenUsers: LiveData<LiveHiddenUsers> by lazy {
live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent -> live.combineWith(getBlockListNote().live().metadata) { localLive, liveMuteListEvent ->
val liveBlockedUsers = (liveMuteListEvent?.note?.event as? PeopleListEvent)?.publicAndPrivateUsers(keyPair.privKey) val blockList = liveMuteListEvent?.note?.event as? PeopleListEvent
LiveHiddenUsers( if (loginWithAmber) {
hiddenUsers = liveBlockedUsers ?: persistentSetOf(), if (blockList?.decryptedContent == null) {
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(), GlobalScope.launch(Dispatchers.IO) {
showSensitiveContent = showSensitiveContent val content = blockList?.content ?: ""
) if (content.isEmpty()) return@launch
AmberUtils.content = ""
AmberUtils.decryptBookmark(
content,
keyPair.pubKey.toHexKey()
)
blockList?.decryptedContent = AmberUtils.content
AmberUtils.content = ""
}
LiveHiddenUsers(
hiddenUsers = persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
} else {
val liveBlockedUsers = blockList.publicAndPrivateUsers(blockList.decryptedContent ?: "")
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers,
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
}
} else {
val liveBlockedUsers = blockList?.publicAndPrivateUsers(keyPair.privKey)
LiveHiddenUsers(
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
showSensitiveContent = showSensitiveContent
)
}
}.distinctUntilChanged() }.distinctUntilChanged()
} }
@@ -1654,6 +1684,28 @@ class Account(
return returningList return returningList
} }
fun hideUser(pubkeyHex: String, encryptedContent: String): PeopleListEvent? {
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
return if (blockList != null) {
PeopleListEvent.addUser(
earlierVersion = blockList,
pubKeyHex = pubkeyHex,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent = encryptedContent
)
} else {
PeopleListEvent.createListWithUser(
name = PeopleListEvent.blockList,
pubKeyHex = pubkeyHex,
isPrivate = true,
pubKey = keyPair.pubKey.toHexKey(),
encryptedContent = encryptedContent
)
}
}
fun hideUser(pubkeyHex: String) { fun hideUser(pubkeyHex: String) {
val blockList = migrateHiddenUsersIfNeeded(getBlockList()) val blockList = migrateHiddenUsersIfNeeded(getBlockList())
@@ -2065,7 +2117,12 @@ class Account(
fun isHidden(userHex: String): Boolean { fun isHidden(userHex: String): Boolean {
val blockList = getBlockList() val blockList = getBlockList()
val decryptedContent = blockList?.decryptedContent ?: ""
if (loginWithAmber) {
if (decryptedContent.isBlank()) return false
return (blockList?.publicAndPrivateUsers(decryptedContent)?.contains(userHex) ?: false) || userHex in transientHiddenUsers
}
return (blockList?.publicAndPrivateUsers(keyPair.privKey)?.contains(userHex) ?: false) || userHex in transientHiddenUsers return (blockList?.publicAndPrivateUsers(keyPair.privKey)?.contains(userHex) ?: false) || userHex in transientHiddenUsers
} }
@@ -14,7 +14,18 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
} }
override fun feed(): List<User> { override fun feed(): List<User> {
return account.getBlockList() val blockList = account.getBlockList()
val decryptedContent = blockList?.decryptedContent ?: ""
if (account.loginWithAmber) {
if (decryptedContent.isEmpty()) return emptyList()
return blockList
?.publicAndPrivateUsers(decryptedContent)
?.map { LocalCache.getOrCreateUser(it) }
?: emptyList()
}
return blockList
?.publicAndPrivateUsers(account.keyPair.privKey) ?.publicAndPrivateUsers(account.keyPair.privKey)
?.map { LocalCache.getOrCreateUser(it) } ?.map { LocalCache.getOrCreateUser(it) }
?: emptyList() ?: emptyList()
@@ -37,6 +37,7 @@ import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.SealedGossipEvent
@@ -472,6 +473,10 @@ class AccountViewModel(val account: Account) : ViewModel() {
} }
} }
fun hide(user: User, encryptedContent: String): PeopleListEvent? {
return account.hideUser(user.pubkeyHex, encryptedContent)
}
fun hide(user: User) { fun hide(user: User) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
account.hideUser(user.pubkeyHex) account.hideUser(user.pubkeyHex)
@@ -1,8 +1,11 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.* import androidx.compose.foundation.*
@@ -53,14 +56,17 @@ import androidx.lifecycle.map
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.AmberUtils
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.actions.SignerDialog import com.vitorpamplona.amethyst.ui.actions.SignerDialog
import com.vitorpamplona.amethyst.ui.actions.SignerType
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
import com.vitorpamplona.amethyst.ui.components.InvoiceRequestCard import com.vitorpamplona.amethyst.ui.components.InvoiceRequestCard
@@ -95,6 +101,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.AppDefinitionEvent import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent
@@ -625,7 +632,9 @@ private fun ProfileHeader(
modifier = Modifier modifier = Modifier
.size(30.dp) .size(30.dp)
.align(Alignment.Center), .align(Alignment.Center),
onClick = { popupExpanded = true }, onClick = {
popupExpanded = true
},
shape = ButtonBorder, shape = ButtonBorder,
colors = ButtonDefaults colors = ButtonDefaults
.buttonColors( .buttonColors(
@@ -1715,6 +1724,80 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
) { ) {
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current
var event by remember { mutableStateOf<Event?>(null) }
if (event != null) {
SignerDialog(
onClose = {
event = null
},
onPost = {
scope.launch(Dispatchers.IO) {
val signedEvent = Event.fromJson(it)
Client.send(signedEvent)
LocalCache.verifyAndConsume(signedEvent, null)
event = null
accountViewModel.account.live.invalidateData()
accountViewModel.account.saveable.invalidateData()
onDismiss()
}
},
data = event!!.toJson()
)
}
val encryptResult = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
onResult = {
if (it.resultCode != Activity.RESULT_OK) {
scope.launch {
Toast.makeText(
context,
"Sign request rejected",
Toast.LENGTH_SHORT
).show()
}
return@rememberLauncherForActivityResult
}
val encryptedContent = it.data?.getStringExtra("signature") ?: ""
event = accountViewModel.hide(user, encryptedContent)
}
)
val decryptResult = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
onResult = {
if (it.resultCode != Activity.RESULT_OK) {
scope.launch {
Toast.makeText(
context,
"Sign request rejected",
Toast.LENGTH_SHORT
).show()
}
return@rememberLauncherForActivityResult
}
val decryptedContent = it.data?.getStringExtra("signature") ?: ""
val blockList = accountViewModel.account.getBlockList()
val privateTags = if (blockList == null) {
listOf(listOf("p", user.pubkeyHex))
} else {
blockList.privateTagsOrEmpty(decryptedContent).plus(element = listOf("p", user.pubkeyHex))
}
val msg = Event.mapper.writeValueAsString(privateTags)
ServiceManager.shouldPauseService = true
AmberUtils.openAmber(
msg,
SignerType.NIP04_ENCRYPT,
encryptResult,
accountViewModel.account.keyPair.pubKey.toHexKey()
)
}
)
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); onDismiss() }) { DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); onDismiss() }) {
Text(stringResource(R.string.copy_user_id)) Text(stringResource(R.string.copy_user_id))
@@ -1730,10 +1813,37 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
Text(stringResource(R.string.unblock_user)) Text(stringResource(R.string.unblock_user))
} }
} else { } else {
DropdownMenuItem(onClick = { DropdownMenuItem(
accountViewModel.hide(user) onClick = {
onDismiss() if (accountViewModel.loggedInWithAmber()) {
}) { scope.launch(Dispatchers.IO) {
val blockList = accountViewModel.account.getBlockList()
val content = blockList?.content ?: ""
if (content.isBlank()) {
val privateTags = listOf(listOf("p", user.pubkeyHex))
val msg = Event.mapper.writeValueAsString(privateTags)
AmberUtils.openAmber(
msg,
SignerType.NIP04_ENCRYPT,
encryptResult,
accountViewModel.account.keyPair.pubKey.toHexKey()
)
} else {
AmberUtils.openAmber(
content,
SignerType.NIP04_DECRYPT,
decryptResult,
accountViewModel.account.keyPair.pubKey.toHexKey()
)
}
}
} else {
accountViewModel.hide(user)
onDismiss()
}
}
) {
Text(stringResource(id = R.string.block_hide_user)) Text(stringResource(id = R.string.block_hide_user))
} }
} }
@@ -70,6 +70,10 @@ abstract class GeneralListEvent(
return privateTags(privKey) ?: emptyList() return privateTags(privKey) ?: emptyList()
} }
fun privateTagsOrEmpty(content: String): List<List<String>> {
return privateTags(content) ?: emptyList()
}
fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] }
fun privateTaggedUsers(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "p" }?.map { it[1] } fun privateTaggedUsers(content: String) = privateTags(content)?.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 privateHashtags(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
@@ -18,6 +18,7 @@ class PeopleListEvent(
content: String, content: String,
sig: HexKey sig: HexKey
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
var decryptedContent: String? = null
var publicAndPrivateUserCache: ImmutableSet<HexKey>? = null var publicAndPrivateUserCache: ImmutableSet<HexKey>? = null
fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet<HexKey> { fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet<HexKey> {
@@ -35,6 +36,20 @@ class PeopleListEvent(
return publicAndPrivateUserCache ?: persistentSetOf() return publicAndPrivateUserCache ?: persistentSetOf()
} }
fun publicAndPrivateUsers(decryptedContent: String): ImmutableSet<HexKey> {
publicAndPrivateUserCache?.let {
return it
}
val privateUserList = privateTagsOrEmpty(decryptedContent).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
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 { fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray): Boolean {
return if (isPrivate) { return if (isPrivate) {
privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == "p" && it[1] == idHex } privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == "p" && it[1] == idHex }
@@ -43,6 +58,14 @@ class PeopleListEvent(
} }
} }
fun isTaggedUser(idHex: String, isPrivate: Boolean, content: String): Boolean {
return if (isPrivate) {
privateTagsOrEmpty(content).any { it.size > 1 && it[0] == "p" && it[1] == idHex }
} else {
isTaggedUser(idHex)
}
}
companion object { companion object {
const val kind = 30000 const val kind = 30000
const val blockList = "mute" const val blockList = "mute"
@@ -65,6 +88,24 @@ class PeopleListEvent(
} }
} }
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptedContent,
tags = listOf(listOf("d", name)),
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = "",
tags = listOf(listOf("d", name), listOf("p", pubKeyHex)),
pubKey = pubKey,
createdAt = createdAt
)
}
}
fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List<String>, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List<String>, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) { return if (isPrivate) {
create( create(
@@ -94,6 +135,24 @@ class PeopleListEvent(
} }
} }
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
return if (isPrivate) {
create(
content = encryptedContent,
tags = earlierVersion.tags,
pubKey = pubKey,
createdAt = createdAt
)
} else {
create(
content = earlierVersion.content,
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
pubKey = pubKey,
createdAt = createdAt
)
}
}
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent { fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
if (earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion if (earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion
@@ -146,5 +205,10 @@ class PeopleListEvent(
val sig = CryptoUtils.sign(id, privateKey) val sig = CryptoUtils.sign(id, privateKey)
return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
fun create(content: String, tags: List<List<String>>, pubKey: HexKey, createdAt: Long = TimeUtils.now()): PeopleListEvent {
val id = generateId(pubKey, createdAt, kind, tags, content)
return PeopleListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
}
} }
} }