From 2fb673acf082e13a95bc2ff7c82610b3e8c0891a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 20 Mar 2023 18:16:07 -0400 Subject: [PATCH] Bookmarks --- amethyst.png | Bin 8556 -> 52614 bytes .../vitorpamplona/amethyst/model/Account.kt | 111 +++++++++++++++++ .../amethyst/model/LocalCache.kt | 13 ++ .../com/vitorpamplona/amethyst/model/User.kt | 13 +- .../service/NostrAccountDataSource.kt | 15 ++- .../amethyst/service/NostrDataSource.kt | 2 + .../service/NostrUserProfileDataSource.kt | 15 ++- .../service/model/BaseTextNoteEvent.kt | 7 -- .../service/model/BookmarkListEvent.kt | 117 ++++++++++++++++++ .../amethyst/service/model/Event.kt | 18 ++- .../amethyst/service/model/LnZapEvent.kt | 9 -- .../service/model/LnZapRequestEvent.kt | 6 - .../amethyst/service/model/MuteListEvent.kt | 110 ++++++++++++++++ .../amethyst/service/model/ReactionEvent.kt | 6 - .../amethyst/service/model/ReportEvent.kt | 7 -- .../amethyst/service/model/RepostEvent.kt | 6 - .../ui/dal/BookmarkPrivateFeedFilter.kt | 25 ++++ .../ui/dal/BookmarkPublicFeedFilter.kt | 20 +++ .../ui/dal/UserProfileBookmarksFeedFilter.kt | 31 +++++ .../amethyst/ui/navigation/AppNavigation.kt | 6 +- .../amethyst/ui/navigation/DrawerContent.kt | 17 ++- .../amethyst/ui/navigation/Routes.kt | 9 +- .../amethyst/ui/note/NoteCompose.kt | 19 +++ .../amethyst/ui/screen/CardFeedViewModel.kt | 2 - .../amethyst/ui/screen/FeedViewModel.kt | 7 ++ .../ui/screen/loggedIn/AccountViewModel.kt | 24 ++++ .../ui/screen/loggedIn/BookmarkListScreen.kt | 92 ++++++++++++++ .../ui/screen/loggedIn/ChannelScreen.kt | 3 + ...{FiltersScreen.kt => HiddenUsersScreen.kt} | 2 +- .../ui/screen/loggedIn/ProfileScreen.kt | 41 +++++- app/src/main/res/values/strings.xml | 10 ++ 31 files changed, 701 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt rename app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/{FiltersScreen.kt => HiddenUsersScreen.kt} (97%) diff --git a/amethyst.png b/amethyst.png index b869584e325e77297af9988affb1c329b8355d93..dd74452c0c91f334f660abedfbdb1902bdd55f1e 100644 GIT binary patch literal 52614 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

>!Ml=wSN`N3@2s3Aj?#}n% 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%0DfD83w~H7Oss#AE#>vC({!Oa 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

T%DaOCG`T(%DB!hi#DxX^=Lu6EC=H0&w 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 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 1d5bed891..b3468c145 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.model import android.content.res.Resources import androidx.core.os.ConfigurationCompat import androidx.lifecycle.LiveData +import com.vitorpamplona.amethyst.service.model.BookmarkListEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent @@ -415,6 +416,116 @@ class Account( joinChannel(event.id) } + fun addPrivateBookmark(note: Note) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList + + val event = BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.plus(note.idHex) ?: listOf(note.idHex), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + fun addPublicBookmark(note: Note) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList + + val event = BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents()?.plus(note.idHex) ?: listOf(note.idHex), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + fun removePrivateBookmark(note: Note) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList + + val event = BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents() ?: emptyList(), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!)?.minus(note.idHex) ?: listOf(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + fun removePublicBookmark(note: Note) { + if (!isWriteable()) return + + val bookmarks = userProfile().latestBookmarkList + + val event = BookmarkListEvent.create( + "bookmark", + bookmarks?.taggedEvents()?.minus(note.idHex), + bookmarks?.taggedUsers() ?: emptyList(), + bookmarks?.taggedAddresses() ?: emptyList(), + + bookmarks?.privateTaggedEvents(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedUsers(privKey = loggedIn.privKey!!) ?: emptyList(), + bookmarks?.privateTaggedAddresses(privKey = loggedIn.privKey!!) ?: emptyList(), + + loggedIn.privKey!! + ) + + Client.send(event) + LocalCache.consume(event) + } + + fun isInPrivateBookmarks(note: Note): Boolean { + if (!isWriteable()) return false + + if (note is AddressableNote) { + return userProfile().latestBookmarkList?.privateTaggedAddresses(loggedIn.privKey!!) + ?.contains(note.address) == true + } else { + return userProfile().latestBookmarkList?.privateTaggedEvents(loggedIn.privKey!!) + ?.contains(note.idHex) == true + } + } + + fun isInPublicBookmarks(note: Note): Boolean { + if (!isWriteable()) return false + + if (note is AddressableNote) { + return userProfile().latestBookmarkList?.taggedAddresses()?.contains(note.address) == true + } else { + return userProfile().latestBookmarkList?.taggedEvents()?.contains(note.idHex) == true + } + } + fun joinChannel(idHex: String) { followingChannels = followingChannels + idHex live.invalidateData() 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 5d13f91ca..f271b31f4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -8,6 +8,7 @@ import com.vitorpamplona.amethyst.service.model.ATag import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent +import com.vitorpamplona.amethyst.service.model.BookmarkListEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -157,6 +158,18 @@ object LocalCache { } } + fun consume(event: BookmarkListEvent) { + val user = getOrCreateUser(event.pubKey) + if (user.latestBookmarkList == null || event.createdAt > user.latestBookmarkList!!.createdAt) { + if (event.dTag() == "bookmark") { + user.updateBookmark(event) + } + // Log.d("MT", "New User Metadata ${oldUser.pubkeyDisplayHex} ${oldUser.toBestDisplayName()}") + } else { + // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") + } + } + fun formattedDateTime(timestamp: Long): String { return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 938810f21..6317fa8ed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource +import com.vitorpamplona.amethyst.service.model.BookmarkListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent @@ -28,6 +29,7 @@ class User(val pubkeyHex: String) { var info: UserMetadata? = null var latestContactList: ContactListEvent? = null + var latestBookmarkList: BookmarkListEvent? = null var notes = setOf() private set @@ -75,6 +77,13 @@ class User(val pubkeyHex: String) { return info?.picture } + fun updateBookmark(event: BookmarkListEvent) { + if (event.id == latestBookmarkList?.id) return + + latestBookmarkList = event + liveSet?.bookmarks?.invalidateData() + } + fun updateContactList(event: ContactListEvent) { if (event.id == latestContactList?.id) return @@ -335,6 +344,7 @@ class UserLiveSet(u: User) { val metadata: UserLiveData = UserLiveData(u) val zaps: UserLiveData = UserLiveData(u) val badges: UserLiveData = UserLiveData(u) + val bookmarks: UserLiveData = UserLiveData(u) fun isInUse(): Boolean { return follows.hasObservers() || @@ -344,7 +354,8 @@ class UserLiveSet(u: User) { relayInfo.hasObservers() || metadata.hasObservers() || zaps.hasObservers() || - badges.hasObservers() + badges.hasObservers() || + bookmarks.hasObservers() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index fac4279b9..749e6ec72 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent +import com.vitorpamplona.amethyst.service.model.BookmarkListEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent @@ -51,6 +52,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { ) } + fun createAccountBookmarkListFilter(): TypedFilter { + return TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(BookmarkListEvent.kind), + authors = listOf(account.userProfile().pubkeyHex), + limit = 1 + ) + ) + } + fun createAccountReportsFilter(): TypedFilter { return TypedFilter( types = FeedType.values().toSet(), @@ -87,7 +99,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { createAccountContactListFilter(), createNotificationFilter(), createAccountReportsFilter(), - createAccountAcceptedAwardsFilter() + createAccountAcceptedAwardsFilter(), + createAccountBookmarkListFilter() ).ifEmpty { null } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 9df9931af..2da6b9223 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent +import com.vitorpamplona.amethyst.service.model.BookmarkListEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent @@ -67,6 +68,7 @@ abstract class NostrDataSource(val debugName: String) { is BadgeAwardEvent -> LocalCache.consume(event) is BadgeDefinitionEvent -> LocalCache.consume(event) is BadgeProfilesEvent -> LocalCache.consume(event) + is BookmarkListEvent -> LocalCache.consume(event) is ChannelCreateEvent -> LocalCache.consume(event) is ChannelHideMessageEvent -> LocalCache.consume(event) is ChannelMessageEvent -> LocalCache.consume(event, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 39c85a97b..d302dddfb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent +import com.vitorpamplona.amethyst.service.model.BookmarkListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.LnZapEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent @@ -90,6 +91,17 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { ) } + fun createBookmarksFilter() = user?.let { + TypedFilter( + types = FeedType.values().toSet(), + filter = JsonFilter( + kinds = listOf(BookmarkListEvent.kind), + authors = listOf(it.pubkeyHex), + limit = 1 + ) + ) + } + fun createReceivedAwardsFilter() = user?.let { TypedFilter( types = FeedType.values().toSet(), @@ -111,7 +123,8 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") { createFollowersFilter(), createUserReceivedZapsFilter(), createAcceptedAwardsFilter(), - createReceivedAwardsFilter() + createReceivedAwardsFilter(), + createBookmarksFilter() ).ifEmpty { null } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt index ce6e4e255..4583e39d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BaseTextNoteEvent.kt @@ -15,13 +15,6 @@ open class BaseTextNoteEvent( fun mentions() = taggedUsers() fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - fun findCitations(): Set { var citations = mutableSetOf() // Removes citations from replies: diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt new file mode 100644 index 000000000..822f12d5b --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt @@ -0,0 +1,117 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.google.gson.reflect.TypeToken +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toByteArray +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +class BookmarkListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + fun address() = ATag(kind, pubKey, dTag(), null) + + fun category() = dTag() + fun bookmarkedPosts() = tags.filter { it[0] == "e" }.mapNotNull { it.getOrNull(1) } + + fun plainContent(privKey: ByteArray): String? { + return try { + val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray()) + + return Utils.decrypt(content, sharedSecret) + } catch (e: Exception) { + Log.w("BookmarkList", "Error decrypting the message ${e.message}") + null + } + } + + @Transient + private var privateTagsCache: List>? = null + + fun privateTags(privKey: ByteArray): List>? { + if (privateTagsCache != null) { + return privateTagsCache + } + + privateTagsCache = try { + gson.fromJson(plainContent(privKey), object : TypeToken>>() {}.type) + } catch (e: Throwable) { + Log.w("BookmarkList", "Error parsing the JSON ${e.message}") + null + } + return privateTagsCache + } + + fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) } + fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "e" }?.mapNotNull { it.getOrNull(1) } + fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } + + companion object { + const val kind = 30001 + + fun create( + name: String = "", + + events: List? = null, + users: List? = null, + addresses: List? = null, + + privEvents: List? = null, + privUsers: List? = null, + privAddresses: List? = null, + + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): BookmarkListEvent { + val pubKey = Utils.pubkeyCreate(privateKey) + + val privTags = mutableListOf>() + privEvents?.forEach { + privTags.add(listOf("e", it)) + } + privUsers?.forEach { + privTags.add(listOf("p", it)) + } + privAddresses?.forEach { + privTags.add(listOf("a", it.toTag())) + } + val msg = gson.toJson(privTags) + + val content = Utils.encrypt( + msg, + privateKey, + pubKey + ) + + 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.toHexKey(), createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return BookmarkListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index 4656d9724..4a4ef3826 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -38,6 +38,14 @@ open class Event( override fun toJson(): String = gson.toJson(this) fun taggedUsers() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } + fun taggedEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } + + fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } fun hashtags() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } @@ -175,7 +183,7 @@ open class Event( BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig) BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig) BadgeProfilesEvent.kind -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig) - + BookmarkListEvent.kind -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig) ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) @@ -206,7 +214,15 @@ open class Event( tags, content ) + + // GSON decided to hardcode these replacements. + // They break Nostr's hash check. + // These lines revert their code. + // https://github.com/google/gson/issues/2295 val rawEventJson = gson.toJson(rawEvent) + .replace("\\u2028", "\u2028") + .replace("\\u2029", "\u2029") + return sha256.digest(rawEventJson.toByteArray()) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index e30336284..ff42b7676 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -23,15 +23,6 @@ class LnZapEvent( .filter { it.firstOrNull() == "p" } .mapNotNull { it.getOrNull(1) } - override fun taggedAddresses(): List = tags - .filter { it.firstOrNull() == "a" } - .mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - override fun amount(): BigDecimal? { return amount } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 2464eda1b..8863b7af4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -15,12 +15,6 @@ class LnZapRequestEvent( ) : Event(id, pubKey, createdAt, kind, tags, content, sig) { fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } companion object { const val kind = 9734 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt new file mode 100644 index 000000000..cb1f8aef7 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt @@ -0,0 +1,110 @@ +package com.vitorpamplona.amethyst.service.model + +import android.util.Log +import com.google.gson.reflect.TypeToken +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toByteArray +import com.vitorpamplona.amethyst.model.toHexKey +import nostr.postr.Utils +import java.util.Date + +class MuteListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig) { + fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" + fun address() = ATag(kind, pubKey, dTag(), null) + + fun plainContent(privKey: ByteArray): String? { + return try { + val sharedSecret = Utils.getSharedSecret(privKey, pubKey.toByteArray()) + + return Utils.decrypt(content, sharedSecret) + } catch (e: Exception) { + Log.w("BookmarkList", "Error decrypting the message ${e.message}") + null + } + } + + @Transient + private var privateTagsCache: List>? = null + + fun privateTags(privKey: ByteArray): List>? { + if (privateTagsCache != null) { + return privateTagsCache + } + + privateTagsCache = try { + gson.fromJson(plainContent(privKey), object : TypeToken>>() {}.type) + } catch (e: Throwable) { + Log.w("BookmarkList", "Error parsing the JSON ${e.message}") + null + } + return privateTagsCache + } + + fun privateTaggedUsers(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "p" }?.mapNotNull { it.getOrNull(1) } + fun privateTaggedEvents(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "e" }?.mapNotNull { it.getOrNull(1) } + fun privateTaggedAddresses(privKey: ByteArray) = privateTags(privKey)?.filter { it.firstOrNull() == "a" }?.mapNotNull { + val aTagValue = it.getOrNull(1) + val relay = it.getOrNull(2) + + if (aTagValue != null) ATag.parse(aTagValue, relay) else null + } + + companion object { + const val kind = 10000 + + fun create( + events: List? = null, + users: List? = null, + addresses: List? = null, + + privEvents: List? = null, + privUsers: List? = null, + privAddresses: List? = null, + + privateKey: ByteArray, + createdAt: Long = Date().time / 1000 + ): MuteListEvent { + val pubKey = Utils.pubkeyCreate(privateKey) + + val privTags = mutableListOf>() + privEvents?.forEach { + privTags.add(listOf("e", it)) + } + privUsers?.forEach { + privTags.add(listOf("p", it)) + } + privAddresses?.forEach { + privTags.add(listOf("a", it.toTag())) + } + val msg = gson.toJson(privTags) + + val content = Utils.encrypt( + msg, + privateKey, + pubKey + ) + + val tags = mutableListOf>() + events?.forEach { + tags.add(listOf("e", it)) + } + users?.forEach { + tags.add(listOf("p", it)) + } + addresses?.forEach { + tags.add(listOf("a", it.toTag())) + } + + val id = generateId(pubKey.toHexKey(), createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return MuteListEvent(id.toHexKey(), pubKey.toHexKey(), createdAt, tags, content, sig.toHexKey()) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index 75c3eee5b..f89bb2977 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -16,12 +16,6 @@ class ReactionEvent( fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } companion object { const val kind = 7 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index 63b51c21b..42f33c3c5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -47,13 +47,6 @@ class ReportEvent( ) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } - companion object { const val kind = 1984 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index ce13a6e5a..d5e0f1eb9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -17,12 +17,6 @@ class RepostEvent( fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { - val aTagValue = it.getOrNull(1) - val relay = it.getOrNull(2) - - if (aTagValue != null) ATag.parse(aTagValue, relay) else null - } fun containedPost() = try { fromJson(content, Client.lenient) 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 new file mode 100644 index 000000000..c748f35ff --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt @@ -0,0 +1,25 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note + +object BookmarkPrivateFeedFilter : FeedFilter() { + lateinit var account: Account + + override fun feed(): List { + val privKey = account.loggedIn.privKey ?: return emptyList() + + val bookmarks = account.userProfile().latestBookmarkList + + val notes = bookmarks?.privateTaggedEvents(privKey) + ?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList() + + val addresses = bookmarks?.privateTaggedAddresses(privKey) + ?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList() + + return notes.plus(addresses) + .sortedBy { it.createdAt() } + .reversed() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt new file mode 100644 index 000000000..5d3ebbabf --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPublicFeedFilter.kt @@ -0,0 +1,20 @@ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note + +object BookmarkPublicFeedFilter : FeedFilter() { + lateinit var account: Account + + override fun feed(): List { + val bookmarks = account.userProfile().latestBookmarkList + + val notes = bookmarks?.taggedEvents()?.mapNotNull { LocalCache.checkGetOrCreateNote(it) } ?: emptyList() + val addresses = bookmarks?.taggedAddresses()?.map { LocalCache.getOrCreateAddressableNote(it) } ?: emptyList() + + return notes.plus(addresses) + .sortedBy { it.createdAt() } + .reversed() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt new file mode 100644 index 000000000..db26758a1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileBookmarksFeedFilter.kt @@ -0,0 +1,31 @@ +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.model.User + +object UserProfileBookmarksFeedFilter : FeedFilter() { + lateinit var account: Account + var user: User? = null + + fun loadUserProfile(accountLoggedIn: Account, userId: String) { + account = accountLoggedIn + user = LocalCache.users[userId] + } + + override fun feed(): List { + val notes = user?.latestBookmarkList?.taggedEvents()?.mapNotNull { + LocalCache.checkGetOrCreateNote(it) + }?.toSet() ?: emptySet() + + val addresses = user?.latestBookmarkList?.taggedAddresses()?.map { + LocalCache.getOrCreateAddressableNote(it) + }?.toSet() ?: emptySet() + + return (notes + addresses) + .filter { account.isAcceptable(it) } + .sortedBy { it.createdAt() } + .reversed() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 321ad9cf2..a4565e4bc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -16,11 +16,12 @@ import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.BookmarkListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomScreen -import com.vitorpamplona.amethyst.ui.screen.loggedIn.FiltersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.HiddenUsersScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.HomeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen @@ -73,7 +74,8 @@ fun AppNavigation( composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, navController) }) composable(Route.Notification.route, content = { NotificationScreen(accountViewModel, navController) }) - composable(Route.Filters.route, content = { FiltersScreen(accountViewModel, navController) }) + composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, navController) }) + composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, navController) }) Route.Profile.let { route -> composable(route.route, route.arguments, content = { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 97a2d883d..defddc0c9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -232,21 +232,26 @@ fun ListContent( scaffoldState = scaffoldState, route = "User/${accountUser.pubkeyHex}" ) - } - Divider(thickness = 0.25.dp) + NavigationRow( + title = stringResource(R.string.bookmarks), + icon = Route.Bookmarks.icon, + tint = MaterialTheme.colors.onBackground, + navController = navController, + scaffoldState = scaffoldState, + route = Route.Bookmarks.route + ) + } NavigationRow( title = stringResource(R.string.security_filters), - icon = Route.Filters.icon, + icon = Route.BlockedUsers.icon, tint = MaterialTheme.colors.onBackground, navController = navController, scaffoldState = scaffoldState, - route = Route.Filters.route + route = Route.BlockedUsers.route ) - Divider(thickness = 0.25.dp) - IconRow( title = stringResource(R.string.backup_keys), icon = R.drawable.ic_key, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt index 2fc3bdd3d..2062c2005 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/Routes.kt @@ -48,11 +48,16 @@ sealed class Route( hasNewItems = { accountViewModel, cache -> messagesHasNewItems(accountViewModel, cache) } ) - object Filters : Route( - route = "Filters", + object BlockedUsers : Route( + route = "BlockedUsers", icon = R.drawable.ic_security ) + object Bookmarks : Route( + route = "Bookmarks", + icon = R.drawable.ic_bookmarks + ) + object Profile : Route( route = "User/{id}", icon = R.drawable.ic_profile, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 2cbc47401..5c1df2b66 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -870,6 +870,25 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, Text(stringResource(R.string.quick_action_share)) } Divider() + if (accountViewModel.isInPrivateBookmarks(note)) { + DropdownMenuItem(onClick = { accountViewModel.removePrivateBookmark(note); onDismiss() }) { + Text(stringResource(R.string.remove_from_private_bookmarks)) + } + } else { + DropdownMenuItem(onClick = { accountViewModel.addPrivateBookmark(note); onDismiss() }) { + Text(stringResource(R.string.add_to_private_bookmarks)) + } + } + if (accountViewModel.isInPublicBookmarks(note)) { + DropdownMenuItem(onClick = { accountViewModel.removePublicBookmark(note); onDismiss() }) { + Text(stringResource(R.string.remove_from_public_bookmarks)) + } + } else { + DropdownMenuItem(onClick = { accountViewModel.addPublicBookmark(note); onDismiss() }) { + Text(stringResource(R.string.add_to_public_bookmarks)) + } + } + Divider() DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) { Text(stringResource(R.string.broadcast)) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index 3060f307a..fa80bee9a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -99,8 +99,6 @@ open class CardFeedViewModel(val dataSource: FeedFilter) : ViewModel() { } } - // val boostCards = boostsPerEvent.map { BoostSetCard(it.key, it.value) } - val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys val multiCards = allBaseNotes.map { MultiSetCard( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index 053ee1ef1..fd0e45cd7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.ViewModel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCacheState import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter +import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChatroomFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter @@ -15,6 +17,7 @@ import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter +import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter @@ -38,11 +41,15 @@ class NostrHashtagFeedViewModel : FeedViewModel(HashtagFeedFilter) class NostrUserProfileNewThreadsFeedViewModel : FeedViewModel(UserProfileNewThreadFeedFilter) class NostrUserProfileConversationsFeedViewModel : FeedViewModel(UserProfileConversationsFeedFilter) class NostrUserProfileReportFeedViewModel : FeedViewModel(UserProfileReportsFeedFilter) +class NostrUserProfileBookmarksFeedViewModel : FeedViewModel(UserProfileBookmarksFeedFilter) class NostrChatroomListKnownFeedViewModel : FeedViewModel(ChatroomListKnownFeedFilter) class NostrChatroomListNewFeedViewModel : FeedViewModel(ChatroomListNewFeedFilter) class NostrHomeFeedViewModel : FeedViewModel(HomeNewThreadFeedFilter) class NostrHomeRepliesFeedViewModel : FeedViewModel(HomeConversationsFeedFilter) +class NostrBookmarkPublicFeedViewModel : FeedViewModel(BookmarkPublicFeedFilter) +class NostrBookmarkPrivateFeedViewModel : FeedViewModel(BookmarkPrivateFeedFilter) + abstract class FeedViewModel(val localFilter: FeedFilter) : ViewModel() { private val _feedContent = MutableStateFlow(FeedState.Loading) val feedContent = _feedContent.asStateFlow() 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 460c5dd4b..3eae24257 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 @@ -85,6 +85,30 @@ class AccountViewModel(private val account: Account) : ViewModel() { account.boost(note) } + fun addPrivateBookmark(note: Note) { + account.addPrivateBookmark(note) + } + + fun addPublicBookmark(note: Note) { + account.addPublicBookmark(note) + } + + fun removePrivateBookmark(note: Note) { + account.removePrivateBookmark(note) + } + + fun removePublicBookmark(note: Note) { + account.removePublicBookmark(note) + } + + fun isInPrivateBookmarks(note: Note): Boolean { + return account.isInPrivateBookmarks(note) + } + + fun isInPublicBookmarks(note: Note): Boolean { + return account.isInPublicBookmarks(note) + } + fun broadcast(note: Note) { account.broadcast(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 new file mode 100644 index 000000000..df3289bca --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/BookmarkListScreen.kt @@ -0,0 +1,92 @@ +package com.vitorpamplona.amethyst.ui.screen.loggedIn + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.google.accompanist.pager.ExperimentalPagerApi +import com.google.accompanist.pager.HorizontalPager +import com.google.accompanist.pager.pagerTabIndicatorOffset +import com.google.accompanist.pager.rememberPagerState +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter +import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter +import com.vitorpamplona.amethyst.ui.screen.FeedView +import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPrivateFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.NostrBookmarkPublicFeedViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalPagerApi::class) +@Composable +fun BookmarkListScreen(accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val account = accountState?.account + + if (account != null) { + BookmarkPublicFeedFilter.account = account + BookmarkPrivateFeedFilter.account = account + + val publicFeedViewModel: NostrBookmarkPublicFeedViewModel = viewModel() + val privateFeedViewModel: NostrBookmarkPrivateFeedViewModel = viewModel() + + val userState by account.userProfile().live().bookmarks.observeAsState() + + LaunchedEffect(userState) { + publicFeedViewModel.invalidateData() + privateFeedViewModel.invalidateData() + } + + Column(Modifier.fillMaxHeight()) { + Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) { + val pagerState = rememberPagerState() + val coroutineScope = rememberCoroutineScope() + + TabRow( + backgroundColor = MaterialTheme.colors.background, + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + Modifier.pagerTabIndicatorOffset(pagerState, tabPositions), + color = MaterialTheme.colors.primary + ) + } + ) { + Tab( + selected = pagerState.currentPage == 0, + onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } }, + text = { + Text(text = stringResource(R.string.private_bookmarks)) + } + ) + Tab( + selected = pagerState.currentPage == 1, + onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } }, + text = { + Text(text = stringResource(R.string.public_bookmarks)) + } + ) + } + HorizontalPager(count = 2, state = pagerState) { + when (pagerState.currentPage) { + 0 -> FeedView(privateFeedViewModel, accountViewModel, navController, null) + 1 -> FeedView(publicFeedViewModel, accountViewModel, navController, null) + } + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index fcc0007db..8520bbab9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -91,6 +91,9 @@ fun ChannelScreen( if (account != null && channelId != null) { val replyTo = remember { mutableStateOf(null) } + ChannelFeedFilter.loadMessagesBetween(account, channelId) + NostrChannelDataSource.loadMessagesBetween(account, channelId) + val channelState by NostrChannelDataSource.channel!!.live.observeAsState() val channel = channelState?.channel ?: return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/FiltersScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt similarity index 97% rename from app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/FiltersScreen.kt rename to app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt index 8f9484d36..436c1b0b0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/FiltersScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HiddenUsersScreen.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.launch @OptIn(ExperimentalPagerApi::class) @Composable -fun FiltersScreen(accountViewModel: AccountViewModel, navController: NavController) { +fun HiddenUsersScreen(accountViewModel: AccountViewModel, navController: NavController) { val accountState by accountViewModel.accountLiveData.observeAsState() val account = accountState?.account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 4c522668a..1e1d59578 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog +import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter @@ -75,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.screen.FeedView import com.vitorpamplona.amethyst.ui.screen.LnZapFeedView +import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileBookmarksFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileConversationsFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowersUserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileFollowsUserFeedViewModel @@ -104,6 +106,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro UserProfileFollowsFeedFilter.loadUserProfile(account, userId) UserProfileZapsFeedFilter.loadUserProfile(userId) UserProfileReportsFeedFilter.loadUserProfile(userId) + UserProfileBookmarksFeedFilter.loadUserProfile(account, userId) NostrUserProfileDataSource.loadUserProfile(userId) @@ -112,7 +115,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro DisposableEffect(accountViewModel) { val observer = LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { - println("Profile Start") + println("Profidle Start") NostrUserProfileDataSource.loadUserProfile(userId) NostrUserProfileDataSource.start() } @@ -225,6 +228,14 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro Text(text = "${showAmount(zapAmount)} ${stringResource(id = R.string.zaps)}") }, + { + val userState by baseUser.live().bookmarks.observeAsState() + val bookmarkList = userState?.user?.latestBookmarkList + val userBookmarks = + (bookmarkList?.taggedEvents()?.count() ?: 0) + (bookmarkList?.taggedAddresses()?.count() ?: 0) + + Text(text = "$userBookmarks ${stringResource(R.string.bookmarks)}") + }, { val userState by baseUser.live().reports.observeAsState() val userReports = userState?.user?.reports?.values?.flatten()?.count() @@ -251,7 +262,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro } } HorizontalPager( - count = 7, + count = 8, state = pagerState, modifier = with(LocalDensity.current) { Modifier.height((columnSize.height - tabsSize.height).toDp()) @@ -263,8 +274,9 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro 2 -> TabFollows(baseUser, accountViewModel, navController) 3 -> TabFollowers(baseUser, accountViewModel, navController) 4 -> TabReceivedZaps(baseUser, accountViewModel, navController) - 5 -> TabReports(baseUser, accountViewModel, navController) - 6 -> TabRelays(baseUser, accountViewModel) + 5 -> TabBookmarks(baseUser, accountViewModel, navController) + 6 -> TabReports(baseUser, accountViewModel, navController) + 7 -> TabRelays(baseUser, accountViewModel) } } } @@ -694,6 +706,27 @@ fun TabNotesConversations(accountViewModel: AccountViewModel, navController: Nav } } +@Composable +fun TabBookmarks(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { + val accountState by accountViewModel.accountLiveData.observeAsState() + val userState by baseUser.live().bookmarks.observeAsState() + if (accountState != null) { + val feedViewModel: NostrUserProfileBookmarksFeedViewModel = viewModel() + + LaunchedEffect(userState) { + feedViewModel.refresh() + } + + Column(Modifier.fillMaxHeight()) { + Column( + modifier = Modifier.padding(vertical = 0.dp) + ) { + FeedView(feedViewModel, accountViewModel, navController, null) + } + } + } +} + @Composable fun TabFollows(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) { val feedViewModel: NostrUserProfileFollowsUserFeedViewModel = viewModel() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 01cc48ba6..bff2399f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -247,4 +247,14 @@ Post Report Block and Report Block + + Bookmarks + Private Bookmarks + Public Bookmarks + + Add to Private Bookmarks + Add to Public Bookmarks + + Remove from Private Bookmarks + Remove from Public Bookmarks