add/remove private bookmarks

This commit is contained in:
greenart7c3
2023-08-25 08:51:23 -03:00
parent 46571a6029
commit e817f94045
10 changed files with 264 additions and 34 deletions

View File

@ -8,6 +8,7 @@ import androidx.core.os.ConfigurationCompat
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import com.vitorpamplona.amethyst.OptOutFromFilters import com.vitorpamplona.amethyst.OptOutFromFilters
import com.vitorpamplona.amethyst.service.AmberUtils
import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.FileHeader
import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
@ -1237,6 +1238,96 @@ class Account(
LocalCache.consume(event) LocalCache.consume(event)
} }
fun addPrivateBookmark(note: Note, decryptedContent: String): BookmarkListEvent? {
val bookmarks = userProfile().latestBookmarkList
val privTags = mutableListOf<List<String>>()
val privEvents = if (note is AddressableNote) {
bookmarks?.privateTaggedEvents(decryptedContent) ?: emptyList()
} else {
bookmarks?.privateTaggedEvents(decryptedContent)?.plus(note.idHex) ?: listOf(note.idHex)
}
val privUsers = bookmarks?.privateTaggedUsers(decryptedContent) ?: emptyList()
val privAddresses = if (note is AddressableNote) {
bookmarks?.privateTaggedAddresses(decryptedContent)?.plus(note.address) ?: listOf(note.address)
} else {
bookmarks?.privateTaggedAddresses(decryptedContent) ?: emptyList()
}
privEvents.forEach {
privTags.add(listOf("e", it))
}
privUsers.forEach {
privTags.add(listOf("p", it))
}
privAddresses.forEach {
privTags.add(listOf("a", it.toTag()))
}
val msg = Event.mapper.writeValueAsString(privTags)
AmberUtils.encryptBookmark(msg, keyPair.pubKey.toHexKey())
if (AmberUtils.content.isBlank()) {
return null
}
return BookmarkListEvent.create(
"bookmark",
bookmarks?.taggedEvents() ?: emptyList(),
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses() ?: emptyList(),
AmberUtils.content,
keyPair.pubKey.toHexKey()
)
}
fun removePrivateBookmark(note: Note, decryptedContent: String): BookmarkListEvent? {
val bookmarks = userProfile().latestBookmarkList
val privTags = mutableListOf<List<String>>()
val privEvents = if (note is AddressableNote) {
bookmarks?.privateTaggedEvents(decryptedContent) ?: emptyList()
} else {
bookmarks?.privateTaggedEvents(decryptedContent)?.minus(note.idHex) ?: listOf(note.idHex)
}
val privUsers = bookmarks?.privateTaggedUsers(decryptedContent) ?: emptyList()
val privAddresses = if (note is AddressableNote) {
bookmarks?.privateTaggedAddresses(decryptedContent)?.minus(note.address) ?: listOf(note.address)
} else {
bookmarks?.privateTaggedAddresses(decryptedContent) ?: emptyList()
}
privEvents.forEach {
privTags.add(listOf("e", it))
}
privUsers.forEach {
privTags.add(listOf("p", it))
}
privAddresses.forEach {
privTags.add(listOf("a", it.toTag()))
}
val msg = Event.mapper.writeValueAsString(privTags)
AmberUtils.encryptBookmark(msg, keyPair.pubKey.toHexKey())
if (AmberUtils.content.isBlank()) {
return null
}
return BookmarkListEvent.create(
"bookmark",
bookmarks?.taggedEvents() ?: emptyList(),
bookmarks?.taggedUsers() ?: emptyList(),
bookmarks?.taggedAddresses() ?: emptyList(),
AmberUtils.content,
keyPair.pubKey.toHexKey()
)
}
fun addPrivateBookmark(note: Note) { fun addPrivateBookmark(note: Note) {
if (!isWriteable()) return if (!isWriteable()) return
@ -1392,14 +1483,24 @@ class Account(
} }
fun isInPrivateBookmarks(note: Note): Boolean { fun isInPrivateBookmarks(note: Note): Boolean {
if (!isWriteable()) return false if (!isWriteable() && !loginWithAmber) return false
if (note is AddressableNote) { if (loginWithAmber) {
return userProfile().latestBookmarkList?.privateTaggedAddresses(keyPair.privKey!!) return if (note is AddressableNote) {
?.contains(note.address) == true userProfile().latestBookmarkList?.privateTaggedAddresses(userProfile().latestBookmarkList?.decryptedContent ?: "")
?.contains(note.address) == true
} else {
userProfile().latestBookmarkList?.privateTaggedEvents(userProfile().latestBookmarkList?.decryptedContent ?: "")
?.contains(note.idHex) == true
}
} else { } else {
return userProfile().latestBookmarkList?.privateTaggedEvents(keyPair.privKey!!) return if (note is AddressableNote) {
?.contains(note.idHex) == true userProfile().latestBookmarkList?.privateTaggedAddresses(keyPair.privKey!!)
?.contains(note.address) == true
} else {
userProfile().latestBookmarkList?.privateTaggedEvents(keyPair.privKey!!)
?.contains(note.idHex) == true
}
} }
} }

View File

@ -4,10 +4,10 @@ import android.util.Log
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.service.AmberUtils
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledInsert import com.vitorpamplona.amethyst.ui.components.BundledInsert
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.Hex import com.vitorpamplona.quartz.encoders.Hex
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
@ -207,7 +207,7 @@ object LocalCache {
if (hexKey != null) { if (hexKey != null) {
val pubKey = Hex.encode(hexKey) val pubKey = Hex.encode(hexKey)
if (pubKey == event.pubKey) { if (pubKey == event.pubKey) {
BookmarkPrivateFeedFilter.content = "" AmberUtils.content = ""
} }
} }
user.updateBookmark(event) user.updateBookmark(event)

View File

@ -0,0 +1,40 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.ui.actions.SignerType
import com.vitorpamplona.amethyst.ui.actions.openAmber
import com.vitorpamplona.quartz.encoders.HexKey
object AmberUtils {
var content: String = ""
var isActivityRunning: Boolean = false
fun decryptBookmark(encryptedContent: String, pubKey: HexKey) {
if (content.isBlank()) {
isActivityRunning = true
openAmber(
encryptedContent,
SignerType.NIP04_DECRYPT,
IntentUtils.decryptActivityResultLauncher,
pubKey
)
while (isActivityRunning) {
Thread.sleep(250)
}
}
}
fun encryptBookmark(decryptedContent: String, pubKey: HexKey) {
if (content.isBlank()) {
isActivityRunning = true
openAmber(
decryptedContent,
SignerType.NIP04_ENCRYPT,
IntentUtils.decryptActivityResultLauncher,
pubKey
)
while (isActivityRunning) {
Thread.sleep(250)
}
}
}
}

View File

@ -25,6 +25,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.AmberUtils
import com.vitorpamplona.amethyst.service.IntentUtils import com.vitorpamplona.amethyst.service.IntentUtils
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
@ -32,7 +33,6 @@ import com.vitorpamplona.amethyst.service.notifications.RegisterAccounts
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting import com.vitorpamplona.amethyst.ui.components.DefaultMutedSetting
import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex import com.vitorpamplona.amethyst.ui.components.keepPlayingMutex
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.navigation.debugState import com.vitorpamplona.amethyst.ui.navigation.debugState
import com.vitorpamplona.amethyst.ui.note.Nip47 import com.vitorpamplona.amethyst.ui.note.Nip47
@ -122,13 +122,13 @@ class MainActivity : AppCompatActivity() {
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
BookmarkPrivateFeedFilter.isActivityRunning = false AmberUtils.isActivityRunning = false
return@registerForActivityResult return@registerForActivityResult
} }
val event = it.data?.getStringExtra("signature") ?: "" val event = it.data?.getStringExtra("signature") ?: ""
BookmarkPrivateFeedFilter.content = event AmberUtils.content = event
BookmarkPrivateFeedFilter.isActivityRunning = false AmberUtils.isActivityRunning = false
} }
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@ -3,15 +3,11 @@ package com.vitorpamplona.amethyst.ui.dal
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.service.IntentUtils import com.vitorpamplona.amethyst.service.AmberUtils
import com.vitorpamplona.amethyst.ui.actions.SignerType
import com.vitorpamplona.amethyst.ui.actions.openAmber
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
object BookmarkPrivateFeedFilter : FeedFilter<Note>() { object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
lateinit var account: Account lateinit var account: Account
var content: String = ""
var isActivityRunning: Boolean = false
override fun feedKey(): String { override fun feedKey(): String {
return account.userProfile().latestBookmarkList?.id ?: "" return account.userProfile().latestBookmarkList?.id ?: ""
@ -21,22 +17,14 @@ object BookmarkPrivateFeedFilter : FeedFilter<Note>() {
val bookmarks = account.userProfile().latestBookmarkList val bookmarks = account.userProfile().latestBookmarkList
if (account.loginWithAmber) { if (account.loginWithAmber) {
if (content.isBlank()) { if (AmberUtils.content.isBlank()) {
isActivityRunning = true AmberUtils.decryptBookmark(bookmarks?.content ?: "", account.keyPair.pubKey.toHexKey())
openAmber( bookmarks?.decryptedContent = AmberUtils.content
bookmarks?.content ?: "",
SignerType.NIP04_DECRYPT,
IntentUtils.decryptActivityResultLauncher,
account.keyPair.pubKey.toHexKey()
)
while (isActivityRunning) {
Thread.sleep(250)
}
} }
val notes = bookmarks?.privateTaggedEvents(content) val notes = bookmarks?.privateTaggedEvents(bookmarks.decryptedContent)
?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList() ?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList()
val addresses = bookmarks?.privateTaggedAddresses(content) val addresses = bookmarks?.privateTaggedAddresses(bookmarks.decryptedContent)
?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList() ?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList()
return notes.plus(addresses).toSet() return notes.plus(addresses).toSet()

View File

@ -41,13 +41,19 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
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.relays.Client
import com.vitorpamplona.amethyst.ui.actions.SignerDialog
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.events.Event
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -390,6 +396,25 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
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
onDismiss()
}
},
data = event!!.toJson()
)
}
if (!state.isFollowingAuthor) { if (!state.isFollowingAuthor) {
DropdownMenuItem(onClick = { DropdownMenuItem(onClick = {
accountViewModel.follow( accountViewModel.follow(
@ -447,11 +472,47 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState<Boolean>, accountVi
} }
Divider() Divider()
if (state.isPrivateBookmarkNote) { if (state.isPrivateBookmarkNote) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePrivateBookmark(note); onDismiss() } }) { DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
if (accountViewModel.loggedInWithAmber()) {
val bookmarks = accountViewModel.userProfile().latestBookmarkList
AmberUtils.decryptBookmark(
bookmarks?.content ?: "",
accountViewModel.account.keyPair.pubKey.toHexKey()
)
bookmarks?.decryptedContent = AmberUtils.content
AmberUtils.content = ""
event = accountViewModel.removePrivateBookmark(note, bookmarks?.decryptedContent ?: "")
} else {
accountViewModel.removePrivateBookmark(note)
onDismiss()
}
}
}
) {
Text(stringResource(R.string.remove_from_private_bookmarks)) Text(stringResource(R.string.remove_from_private_bookmarks))
} }
} else { } else {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPrivateBookmark(note); onDismiss() } }) { DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
if (accountViewModel.loggedInWithAmber()) {
val bookmarks = accountViewModel.userProfile().latestBookmarkList
AmberUtils.decryptBookmark(
bookmarks?.content ?: "",
accountViewModel.account.keyPair.pubKey.toHexKey()
)
bookmarks?.decryptedContent = AmberUtils.content
AmberUtils.content = ""
event = accountViewModel.addPrivateBookmark(note, bookmarks?.decryptedContent ?: "")
} else {
accountViewModel.addPrivateBookmark(note)
onDismiss()
}
}
}
) {
Text(stringResource(R.string.add_to_private_bookmarks)) Text(stringResource(R.string.add_to_private_bookmarks))
} }
} }

View File

@ -21,6 +21,7 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.BookmarkListEvent
import com.vitorpamplona.quartz.events.DeletionEvent import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.GiftWrapEvent import com.vitorpamplona.quartz.events.GiftWrapEvent
@ -231,10 +232,18 @@ class AccountViewModel(val account: Account) : ViewModel() {
account.addPrivateBookmark(note) account.addPrivateBookmark(note)
} }
fun addPrivateBookmark(note: Note, decryptedContent: String): BookmarkListEvent? {
return account.addPrivateBookmark(note, decryptedContent)
}
fun addPublicBookmark(note: Note) { fun addPublicBookmark(note: Note) {
account.addPublicBookmark(note) account.addPublicBookmark(note)
} }
fun removePrivateBookmark(note: Note, decryptedContent: String): BookmarkListEvent? {
return account.removePrivateBookmark(note, decryptedContent)
}
fun removePrivateBookmark(note: Note) { fun removePrivateBookmark(note: Note) {
account.removePrivateBookmark(note) account.removePrivateBookmark(note)
} }

View File

@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.AmberUtils
import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter
import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter
import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel
@ -95,7 +96,7 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
BookmarkPrivateFeedFilter.content = "" AmberUtils.content = ""
} }
} }
} }

View File

@ -16,9 +16,37 @@ class BookmarkListEvent(
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 = ""
companion object { companion object {
const val kind = 30001 const val kind = 30001
fun create(
name: String = "",
events: List<String>? = null,
users: List<String>? = null,
addresses: List<ATag>? = null,
content: String,
pubKey: HexKey,
createdAt: Long = TimeUtils.now()
): BookmarkListEvent {
val tags = mutableListOf<List<String>>()
tags.add(listOf("d", name))
events?.forEach {
tags.add(listOf("e", it))
}
users?.forEach {
tags.add(listOf("p", it))
}
addresses?.forEach {
tags.add(listOf("a", it.toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
return BookmarkListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "")
}
fun create( fun create(
name: String = "", name: String = "",

View File

@ -71,9 +71,11 @@ abstract class GeneralListEvent(
} }
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 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] }
fun privateHashtags(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "t" }?.map { it[1] }
fun privateGeohashes(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] } fun privateGeohashes(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] }
fun privateGeohashes(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "g" }?.map { it[1] }
fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] } fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }
fun privateTaggedEvents(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] } fun privateTaggedEvents(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }