diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 22b0b6784..3f9307157 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -8,6 +8,7 @@ import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData import androidx.lifecycle.distinctUntilChanged import com.vitorpamplona.amethyst.OptOutFromFilters +import com.vitorpamplona.amethyst.service.AmberUtils import com.vitorpamplona.amethyst.service.FileHeader import com.vitorpamplona.amethyst.service.NostrLnZapPaymentResponseDataSource import com.vitorpamplona.amethyst.service.relays.Client @@ -1237,6 +1238,96 @@ class Account( LocalCache.consume(event) } + fun addPrivateBookmark(note: Note, decryptedContent: String): BookmarkListEvent? { + val bookmarks = userProfile().latestBookmarkList + val privTags = mutableListOf>() + + 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>() + + 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) { if (!isWriteable()) return @@ -1392,14 +1483,24 @@ class Account( } fun isInPrivateBookmarks(note: Note): Boolean { - if (!isWriteable()) return false + if (!isWriteable() && !loginWithAmber) return false - if (note is AddressableNote) { - return userProfile().latestBookmarkList?.privateTaggedAddresses(keyPair.privKey!!) - ?.contains(note.address) == true + if (loginWithAmber) { + return if (note is AddressableNote) { + userProfile().latestBookmarkList?.privateTaggedAddresses(userProfile().latestBookmarkList?.decryptedContent ?: "") + ?.contains(note.address) == true + } else { + userProfile().latestBookmarkList?.privateTaggedEvents(userProfile().latestBookmarkList?.decryptedContent ?: "") + ?.contains(note.idHex) == true + } } else { - return userProfile().latestBookmarkList?.privateTaggedEvents(keyPair.privKey!!) - ?.contains(note.idHex) == true + return if (note is AddressableNote) { + userProfile().latestBookmarkList?.privateTaggedAddresses(keyPair.privKey!!) + ?.contains(note.address) == true + } else { + userProfile().latestBookmarkList?.privateTaggedEvents(keyPair.privKey!!) + ?.contains(note.idHex) == true + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index cbe809125..ec6b18328 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -4,10 +4,10 @@ import android.util.Log import androidx.compose.runtime.Stable import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences +import com.vitorpamplona.amethyst.service.AmberUtils import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.relays.Relay 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.Hex import com.vitorpamplona.quartz.encoders.HexKey @@ -207,7 +207,7 @@ object LocalCache { if (hexKey != null) { val pubKey = Hex.encode(hexKey) if (pubKey == event.pubKey) { - BookmarkPrivateFeedFilter.content = "" + AmberUtils.content = "" } } user.updateBookmark(event) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/AmberUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/AmberUtils.kt new file mode 100644 index 000000000..75cda65a1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/AmberUtils.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 0aa63f34a..e39b7228f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.ServiceManager +import com.vitorpamplona.amethyst.service.AmberUtils import com.vitorpamplona.amethyst.service.IntentUtils import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus 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.ui.components.DefaultMutedSetting 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.debugState import com.vitorpamplona.amethyst.ui.note.Nip47 @@ -122,13 +122,13 @@ class MainActivity : AppCompatActivity() { Toast.LENGTH_SHORT ).show() } - BookmarkPrivateFeedFilter.isActivityRunning = false + AmberUtils.isActivityRunning = false return@registerForActivityResult } val event = it.data?.getStringExtra("signature") ?: "" - BookmarkPrivateFeedFilter.content = event - BookmarkPrivateFeedFilter.isActivityRunning = false + AmberUtils.content = event + AmberUtils.isActivityRunning = false } super.onCreate(savedInstanceState) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt index bee80ce76..b059bd829 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt @@ -3,15 +3,11 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note -import com.vitorpamplona.amethyst.service.IntentUtils -import com.vitorpamplona.amethyst.ui.actions.SignerType -import com.vitorpamplona.amethyst.ui.actions.openAmber +import com.vitorpamplona.amethyst.service.AmberUtils import com.vitorpamplona.quartz.encoders.toHexKey object BookmarkPrivateFeedFilter : FeedFilter() { lateinit var account: Account - var content: String = "" - var isActivityRunning: Boolean = false override fun feedKey(): String { return account.userProfile().latestBookmarkList?.id ?: "" @@ -21,22 +17,14 @@ object BookmarkPrivateFeedFilter : FeedFilter() { val bookmarks = account.userProfile().latestBookmarkList if (account.loginWithAmber) { - if (content.isBlank()) { - isActivityRunning = true - openAmber( - bookmarks?.content ?: "", - SignerType.NIP04_DECRYPT, - IntentUtils.decryptActivityResultLauncher, - account.keyPair.pubKey.toHexKey() - ) - while (isActivityRunning) { - Thread.sleep(250) - } + if (AmberUtils.content.isBlank()) { + AmberUtils.decryptBookmark(bookmarks?.content ?: "", account.keyPair.pubKey.toHexKey()) + bookmarks?.decryptedContent = AmberUtils.content } - val notes = bookmarks?.privateTaggedEvents(content) + val notes = bookmarks?.privateTaggedEvents(bookmarks.decryptedContent) ?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList() - val addresses = bookmarks?.privateTaggedAddresses(content) + val addresses = bookmarks?.privateTaggedAddresses(bookmarks.decryptedContent) ?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList() return notes.plus(addresses).toSet() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index a7bb6a9fa..5552e2013 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -41,13 +41,19 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note 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.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog 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.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -390,6 +396,25 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi val scope = rememberCoroutineScope() + var event by remember { mutableStateOf(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) { DropdownMenuItem(onClick = { accountViewModel.follow( @@ -447,11 +472,47 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi } Divider() 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)) } } 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)) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 169920c09..d565e35a6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -21,6 +21,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.UserState import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.events.BookmarkListEvent import com.vitorpamplona.quartz.events.DeletionEvent import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.GiftWrapEvent @@ -231,10 +232,18 @@ class AccountViewModel(val account: Account) : ViewModel() { account.addPrivateBookmark(note) } + fun addPrivateBookmark(note: Note, decryptedContent: String): BookmarkListEvent? { + return account.addPrivateBookmark(note, decryptedContent) + } + fun addPublicBookmark(note: Note) { account.addPublicBookmark(note) } + fun removePrivateBookmark(note: Note, decryptedContent: String): BookmarkListEvent? { + return account.removePrivateBookmark(note, decryptedContent) + } + fun removePrivateBookmark(note: Note) { account.removePrivateBookmark(note) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt index 97890ab3f..d4b5dc1bb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.service.AmberUtils import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel @@ -95,7 +96,7 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit DisposableEffect(Unit) { onDispose { - BookmarkPrivateFeedFilter.content = "" + AmberUtils.content = "" } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt index f9d161baa..094f9b491 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/BookmarkListEvent.kt @@ -16,9 +16,37 @@ class BookmarkListEvent( content: String, sig: HexKey ) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) { + var decryptedContent = "" + companion object { const val kind = 30001 + fun create( + name: String = "", + events: List? = null, + users: List? = null, + addresses: List? = null, + content: String, + pubKey: HexKey, + createdAt: Long = TimeUtils.now() + ): BookmarkListEvent { + val tags = mutableListOf>() + tags.add(listOf("d", name)) + + events?.forEach { + tags.add(listOf("e", it)) + } + users?.forEach { + tags.add(listOf("p", it)) + } + addresses?.forEach { + tags.add(listOf("a", it.toTag())) + } + + val id = generateId(pubKey, createdAt, kind, tags, content) + return BookmarkListEvent(id.toHexKey(), pubKey, createdAt, tags, content, "") + } + fun create( name: String = "", diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index b6df2d35e..54a895a02 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -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(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(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(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(content: String) = privateTags(content)?.filter { it.size > 1 && it[0] == "e" }?.map { it[1] }