From b40b37b6ac7c89a2c385aa61fcece285b2d6c7c0 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 25 Feb 2025 18:09:04 -0500 Subject: [PATCH] Adds the logged in user' posts to a user in the user's profile. Refactors the user profile screen in many packages. --- .../ui/dal/UserProfileMutualFeedFilter.kt | 87 + .../ui/dal/UserProfileZapsFeedFilter.kt | 2 +- .../amethyst/ui/note/ZapNoteCompose.kt | 6 +- .../amethyst/ui/screen/FeedViewModel.kt | 85 - .../amethyst/ui/screen/UserFeedViewModel.kt | 30 - .../screen/loggedIn/profile/FollowButtons.kt | 71 + .../screen/loggedIn/profile/ProfileScreen.kt | 1653 +---------------- .../profile/bookmarks/BookmarkTabHeader.kt | 54 + .../NostrUserProfileBookmarksFeedViewModel.kt | 42 + .../profile/bookmarks/TabBookmarks.kt | 55 + ...trUserProfileConversationsFeedViewModel.kt | 42 + .../conversations/TabNotesConversations.kt | 46 + .../profile/followers/FollowersTabHeader.kt | 55 + ...trUserProfileFollowersUserFeedViewModel.kt | 42 + .../profile/followers/TabFollowers.kt | 58 + .../profile/follows/FollowTabHeader.kt | 55 + ...ostrUserProfileFollowsUserFeedViewModel.kt | 42 + .../loggedIn/profile/follows/TabFollows.kt | 58 + .../NostrUserProfileGalleryFeedViewModel.kt | 42 + .../loggedIn/profile/gallery/TabGallery.kt | 51 + .../profile/hashtags/FollowedTagsTabHeader.kt | 44 + .../profile/hashtags/TabFollowedTags.kt | 70 + .../header/DisplayFollowUnfollowButton.kt | 92 + .../profile/header/DisplayLNAddress.kt | 141 ++ .../profile/header/DrawAdditionalInfo.kt | 260 +++ .../loggedIn/profile/header/DrawBanner.kt | 94 + .../loggedIn/profile/header/EditButton.kt | 67 + .../loggedIn/profile/header/MessageButton.kt | 68 + .../loggedIn/profile/header/ProfileActions.kt | 53 + .../loggedIn/profile/header/ProfileHeader.kt | 175 ++ .../profile/header/UserProfileDropDownMenu.kt | 152 ++ .../header/apps/DisplayAppRecommendations.kt | 84 + ...ostrUserAppRecommendationsFeedViewModel.kt | 39 + .../loggedIn/profile/header/apps/WatchApp.kt | 87 + .../profile/header/badges/DisplayBadges.kt | 242 +++ .../NostrUserProfileMutualFeedViewModel.kt | 42 + .../profile/mutual/TabMutualConversations.kt | 46 + ...NostrUserProfileNewThreadsFeedViewModel.kt | 42 + .../profile/newthreads/TabNotesNewThreads.kt | 46 + .../profile/{ => relays}/RelayFeedView.kt | 2 +- .../{ => relays}/RelayFeedViewModel.kt | 2 +- .../profile/relays/RelaysTabHeader.kt | 48 + .../loggedIn/profile/relays/TabRelays.kt | 75 + .../NostrUserProfileReportFeedViewModel.kt | 37 + .../profile/reports/ReportsTabHeader.kt | 54 + .../loggedIn/profile/reports/TabReports.kt | 50 + .../reports/WatchReportsAndUpdateFeed.kt | 36 + .../profile/{ => zaps}/LnZapFeedState.kt | 2 +- .../profile/{ => zaps}/LnZapFeedView.kt | 2 +- .../profile/{ => zaps}/LnZapFeedViewModel.kt | 15 +- .../zaps/NostrUserProfileZapsFeedViewModel.kt | 36 + .../loggedIn/profile/zaps/ShowUserButton.kt | 51 + .../loggedIn/profile/zaps/TabReceivedZaps.kt | 43 + .../profile/zaps/WatchIsHiddenUser.kt | 43 + .../profile/zaps/WatchZapsAndUpdateFeed.kt | 37 + .../loggedIn/profile/zaps/ZapTabHeader.kt | 54 + amethyst/src/main/res/values/strings.xml | 1 + .../nip01Core/tags/addressables/ATag.kt | 3 + .../quartz/nip51Lists/BookmarkListEvent.kt | 3 + 59 files changed, 3299 insertions(+), 1715 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileMutualFeedFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/FollowButtons.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/NostrUserProfileBookmarksFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/TabBookmarks.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/NostrUserProfileConversationsFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/TabNotesConversations.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/NostrUserProfileFollowersUserFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/NostrUserProfileFollowsUserFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/NostrUserProfileGalleryFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/TabGallery.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/NostrUserAppRecommendationsFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/NostrUserProfileMutualFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/TabMutualConversations.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/NostrUserProfileNewThreadsFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/{ => relays}/RelayFeedView.kt (97%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/{ => relays}/RelayFeedViewModel.kt (98%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/NostrUserProfileReportFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/{ => zaps}/LnZapFeedState.kt (96%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/{ => zaps}/LnZapFeedView.kt (98%) rename amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/{ => zaps}/LnZapFeedViewModel.kt (86%) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/NostrUserProfileZapsFeedViewModel.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ShowUserButton.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchIsHiddenUser.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileMutualFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileMutualFeedFilter.kt new file mode 100644 index 000000000..04ebcd61c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileMutualFeedFilter.kt @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.quartz.experimental.audio.header.AudioHeaderEvent +import com.vitorpamplona.quartz.experimental.audio.track.AudioTrackEvent +import com.vitorpamplona.quartz.experimental.interactiveStories.InteractiveStoryPrologueEvent +import com.vitorpamplona.quartz.experimental.zapPolls.PollNoteEvent +import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent +import com.vitorpamplona.quartz.nip01Core.tags.people.isTaggedUser +import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent +import com.vitorpamplona.quartz.nip23LongContent.LongTextNoteEvent +import com.vitorpamplona.quartz.nip35Torrents.TorrentEvent +import com.vitorpamplona.quartz.nip54Wiki.WikiNoteEvent +import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent +import com.vitorpamplona.quartz.nip99Classifieds.ClassifiedsEvent + +class UserProfileMutualFeedFilter( + val user: User, + val account: Account, +) : AdditiveFeedFilter() { + override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + user.pubkeyHex + + override fun feed(): List { + val notes = + LocalCache.notes.filterIntoSet { _, it -> + it !is AddressableNote && it.event !is AddressableEvent && acceptableEvent(it) + } + + val longFormNotes = + LocalCache.addressables.filterIntoSet { _, it -> + acceptableEvent(it) + } + + return sort(notes + longFormNotes) + } + + override fun applyFilter(collection: Set): Set = innerApplyFilter(collection) + + private fun innerApplyFilter(collection: Collection): Set = collection.filterTo(HashSet()) { acceptableEvent(it) } + + fun acceptableEvent(it: Note): Boolean = + it.author == account.userProfile() && + ( + it.event is TextNoteEvent || + it.event is ClassifiedsEvent || + it.event is RepostEvent || + it.event is GenericRepostEvent || + it.event is LongTextNoteEvent || + it.event is WikiNoteEvent || + it.event is PollNoteEvent || + it.event is HighlightEvent || + it.event is InteractiveStoryPrologueEvent || + it.event is AudioTrackEvent || + it.event is AudioHeaderEvent || + it.event is TorrentEvent + ) && it.event?.isTaggedUser(user.pubkeyHex) == true + + override fun sort(collection: Set): List = collection.sortedWith(DefaultFeedOrder) + + override fun limit() = 200 +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt index ac1c7c007..ebb0d7567 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/dal/UserProfileZapsFeedFilter.kt @@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.dal import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ZapReqResponse +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent class UserProfileZapsFeedFilter( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index 385502963..73efdf414 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -48,10 +48,10 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.navigation.INav import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ShowUserButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.WatchIsHiddenUser -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ZapReqResponse +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.WatchIsHiddenUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapReqResponse import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.placeholderText diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index f63c4c79b..6f8297b2e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -36,7 +36,6 @@ import com.vitorpamplona.amethyst.model.Channel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.ThreadLevelCalculator -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.ui.dal.BookmarkPrivateFeedFilter import com.vitorpamplona.amethyst.ui.dal.BookmarkPublicFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter @@ -48,12 +47,6 @@ import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter import com.vitorpamplona.amethyst.ui.dal.NIP90ContentDiscoveryResponseFilter import com.vitorpamplona.amethyst.ui.dal.ThreadFeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileGalleryFeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.feeds.FeedContentState import com.vitorpamplona.amethyst.ui.feeds.FeedState import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent @@ -109,34 +102,6 @@ class NostrThreadFeedViewModel( } } -class NostrUserProfileNewThreadsFeedViewModel( - val user: User, - val account: Account, -) : FeedViewModel(UserProfileNewThreadFeedFilter(user, account)) { - class Factory( - val user: User, - val account: Account, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserProfileNewThreadsFeedViewModel = - NostrUserProfileNewThreadsFeedViewModel(user, account) - as NostrUserProfileNewThreadsFeedViewModel - } -} - -class NostrUserProfileConversationsFeedViewModel( - val user: User, - val account: Account, -) : FeedViewModel(UserProfileConversationsFeedFilter(user, account)) { - class Factory( - val user: User, - val account: Account, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserProfileConversationsFeedViewModel = - NostrUserProfileConversationsFeedViewModel(user, account) - as NostrUserProfileConversationsFeedViewModel - } -} - class NostrHashtagFeedViewModel( val hashtag: String, val account: Account, @@ -173,44 +138,6 @@ class NostrCommunityFeedViewModel( } } -class NostrUserProfileReportFeedViewModel( - val user: User, -) : FeedViewModel(UserProfileReportsFeedFilter(user)) { - class Factory( - val user: User, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserProfileReportFeedViewModel = NostrUserProfileReportFeedViewModel(user) as NostrUserProfileReportFeedViewModel - } -} - -class NostrUserProfileGalleryFeedViewModel( - val user: User, - val account: Account, -) : FeedViewModel(UserProfileGalleryFeedFilter(user, account)) { - class Factory( - val user: User, - val account: Account, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserProfileGalleryFeedViewModel = - NostrUserProfileGalleryFeedViewModel(user, account) - as NostrUserProfileGalleryFeedViewModel - } -} - -class NostrUserProfileBookmarksFeedViewModel( - val user: User, - val account: Account, -) : FeedViewModel(UserProfileBookmarksFeedFilter(user, account)) { - class Factory( - val user: User, - val account: Account, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserProfileBookmarksFeedViewModel = - NostrUserProfileBookmarksFeedViewModel(user, account) - as NostrUserProfileBookmarksFeedViewModel - } -} - @Stable class NostrBookmarkPublicFeedViewModel( val account: Account, @@ -259,18 +186,6 @@ class NostrDraftEventsFeedViewModel( } } -class NostrUserAppRecommendationsFeedViewModel( - val user: User, -) : FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) { - class Factory( - val user: User, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserAppRecommendationsFeedViewModel = - NostrUserAppRecommendationsFeedViewModel(user) - as NostrUserAppRecommendationsFeedViewModel - } -} - abstract class LevelFeedViewModel( localFilter: FeedFilter, ) : FeedViewModel(localFilter) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 9873b40f3..3027ee01a 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -34,8 +34,6 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter import com.vitorpamplona.amethyst.ui.dal.SpammerAccountsFeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter import com.vitorpamplona.amethyst.ui.feeds.InvalidatableContent import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.ammolite.relays.BundledUpdate @@ -48,34 +46,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class NostrUserProfileFollowsUserFeedViewModel( - val user: User, - val account: Account, -) : UserFeedViewModel(UserProfileFollowsFeedFilter(user, account)) { - class Factory( - val user: User, - val account: Account, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserProfileFollowsUserFeedViewModel = - NostrUserProfileFollowsUserFeedViewModel(user, account) - as NostrUserProfileFollowsUserFeedViewModel - } -} - -class NostrUserProfileFollowersUserFeedViewModel( - val user: User, - val account: Account, -) : UserFeedViewModel(UserProfileFollowersFeedFilter(user, account)) { - class Factory( - val user: User, - val account: Account, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserProfileFollowersUserFeedViewModel = - NostrUserProfileFollowersUserFeedViewModel(user, account) - as NostrUserProfileFollowersUserFeedViewModel - } -} - class NostrHiddenAccountsFeedViewModel( val account: Account, ) : UserFeedViewModel(HiddenAccountsFeedFilter(account)) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/FollowButtons.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/FollowButtons.kt new file mode 100644 index 000000000..70a5f3d9a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/FollowButtons.kt @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.ButtonPadding + +@Composable +fun FollowButton( + text: Int = R.string.follow, + onClick: () -> Unit, +) { + Button( + modifier = Modifier.padding(start = 3.dp), + onClick = onClick, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + contentPadding = ButtonPadding, + ) { + Text(text = stringRes(text), color = Color.White, textAlign = TextAlign.Center) + } +} + +@Composable +fun UnfollowButton(onClick: () -> Unit) { + Button( + modifier = Modifier.padding(horizontal = 3.dp), + onClick = onClick, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + contentPadding = ButtonPadding, + ) { + Text(text = stringRes(R.string.unfollow), color = Color.White) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt index 4231abf3d..65a8f5abc 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/ProfileScreen.kt @@ -20,53 +20,21 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile -import android.content.Intent -import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.scrollBy -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.CutCornerShape -import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material.icons.filled.EditNote -import androidx.compose.material.icons.filled.Link -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Surface @@ -75,129 +43,66 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.distinctUntilChanged import androidx.lifecycle.map import androidx.lifecycle.viewmodel.compose.viewModel -import coil3.compose.AsyncImage import com.vitorpamplona.amethyst.R -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import com.vitorpamplona.amethyst.model.AddressableNote -import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource -import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled -import com.vitorpamplona.amethyst.ui.actions.InformationDialog -import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji -import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus -import com.vitorpamplona.amethyst.ui.components.InvoiceRequestCard -import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage -import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage -import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer -import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog -import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter -import com.vitorpamplona.amethyst.ui.feeds.FeedState -import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.ui.navigation.INav -import com.vitorpamplona.amethyst.ui.navigation.Route -import com.vitorpamplona.amethyst.ui.navigation.routeToMessage -import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture -import com.vitorpamplona.amethyst.ui.note.DrawPlayName -import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog -import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon -import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote -import com.vitorpamplona.amethyst.ui.note.externalLinkForUser -import com.vitorpamplona.amethyst.ui.note.payViaIntent -import com.vitorpamplona.amethyst.ui.note.showAmountInteger -import com.vitorpamplona.amethyst.ui.screen.NostrUserAppRecommendationsFeedViewModel -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 -import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileGalleryFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileNewThreadsFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.NostrUserProfileReportFeedViewModel -import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView -import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView -import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState -import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.RenderGalleryFeed -import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.BookmarkTabHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.NostrUserProfileBookmarksFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.TabBookmarks +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.NostrUserProfileConversationsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations.TabNotesConversations +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.FollowersTabHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.NostrUserProfileFollowersUserFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers.TabFollowers +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.FollowTabHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.NostrUserProfileFollowsUserFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows.TabFollows +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.NostrUserProfileGalleryFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.TabGallery +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags.FollowedTagsTabHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags.TabFollowedTags +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.ProfileHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.NostrUserAppRecommendationsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.NostrUserProfileMutualFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.TabMutualConversations +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.NostrUserProfileNewThreadsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.RelaysTabHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.TabRelays +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.NostrUserProfileReportFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.ReportsTabHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.TabReports +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.NostrUserProfileZapsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.TabReceivedZaps +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ZapTabHeader import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange -import com.vitorpamplona.amethyst.ui.theme.ButtonBorder -import com.vitorpamplona.amethyst.ui.theme.ButtonPadding import com.vitorpamplona.amethyst.ui.theme.DividerThickness -import com.vitorpamplona.amethyst.ui.theme.Size100dp -import com.vitorpamplona.amethyst.ui.theme.Size15Modifier -import com.vitorpamplona.amethyst.ui.theme.Size16Modifier -import com.vitorpamplona.amethyst.ui.theme.Size25Modifier -import com.vitorpamplona.amethyst.ui.theme.Size35dp -import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.ZeroPadding -import com.vitorpamplona.amethyst.ui.theme.placeholderText -import com.vitorpamplona.amethyst.ui.theme.userProfileBorderModifier -import com.vitorpamplona.quartz.nip01Core.tags.addressables.taggedATags -import com.vitorpamplona.quartz.nip01Core.tags.events.ETag -import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents -import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList -import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity -import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaim -import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity -import com.vitorpamplona.quartz.nip39ExtIdentities.TelegramIdentity -import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity -import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims -import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse -import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse -import com.vitorpamplona.quartz.nip56Reports.ReportEvent -import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent -import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent -import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.math.BigDecimal @Composable fun ProfileScreen( @@ -304,6 +209,16 @@ fun PrepareViewModels( ), ) + val mutualViewModel: NostrUserProfileMutualFeedViewModel = + viewModel( + key = baseUser.pubkeyHex + "UserProfileMutualFeedViewModel", + factory = + NostrUserProfileMutualFeedViewModel.Factory( + baseUser, + accountViewModel.account, + ), + ) + val bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel = viewModel( key = baseUser.pubkeyHex + "UserProfileBookmarksFeedViewModel", @@ -327,6 +242,7 @@ fun PrepareViewModels( baseUser = baseUser, threadsViewModel, repliesViewModel, + mutualViewModel, followsFeedViewModel, followersFeedViewModel, appRecommendations, @@ -344,6 +260,7 @@ fun ProfileScreen( baseUser: User, threadsViewModel: NostrUserProfileNewThreadsFeedViewModel, repliesViewModel: NostrUserProfileConversationsFeedViewModel, + mutualViewModel: NostrUserProfileMutualFeedViewModel, followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel, followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, appRecommendations: NostrUserAppRecommendationsFeedViewModel, @@ -385,37 +302,29 @@ fun ProfileScreen( onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) } } - RenderSurface( - baseUser, - threadsViewModel, - repliesViewModel, - appRecommendations, - followsFeedViewModel, - followersFeedViewModel, - zapFeedViewModel, - bookmarksFeedViewModel, - galleryFeedViewModel, - reportsFeedViewModel, - accountViewModel, - nav, - ) + RenderSurface { tabRowModifier: Modifier, pagerModifier: Modifier -> + RenderScreen( + baseUser, + tabRowModifier, + pagerModifier, + threadsViewModel, + repliesViewModel, + mutualViewModel, + appRecommendations, + followsFeedViewModel, + followersFeedViewModel, + zapFeedViewModel, + bookmarksFeedViewModel, + galleryFeedViewModel, + reportsFeedViewModel, + accountViewModel, + nav, + ) + } } @Composable -private fun RenderSurface( - baseUser: User, - threadsViewModel: NostrUserProfileNewThreadsFeedViewModel, - repliesViewModel: NostrUserProfileConversationsFeedViewModel, - appRecommendations: NostrUserAppRecommendationsFeedViewModel, - followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel, - followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, - zapFeedViewModel: NostrUserProfileZapsFeedViewModel, - bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel, - galleryFeedViewModel: NostrUserProfileGalleryFeedViewModel, - reportsFeedViewModel: NostrUserProfileReportFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { +private fun RenderSurface(content: @Composable (tabRowModifier: Modifier, pagerModifier: Modifier) -> Unit) { Surface( modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.background, @@ -480,35 +389,20 @@ private fun RenderSurface( ).fillMaxHeight() }, ) { - RenderScreen( - baseUser, - tabRowModifier, - pagerModifier, - threadsViewModel, - repliesViewModel, - appRecommendations, - followsFeedViewModel, - followersFeedViewModel, - zapFeedViewModel, - bookmarksFeedViewModel, - galleryFeedViewModel, - reportsFeedViewModel, - accountViewModel, - nav, - ) + content(tabRowModifier, pagerModifier) } } } } @Composable -@OptIn(ExperimentalFoundationApi::class) private fun RenderScreen( baseUser: User, tabRowModifier: Modifier, pagerModifier: Modifier, threadsViewModel: NostrUserProfileNewThreadsFeedViewModel, repliesViewModel: NostrUserProfileConversationsFeedViewModel, + mutualViewModel: NostrUserProfileMutualFeedViewModel, appRecommendations: NostrUserAppRecommendationsFeedViewModel, followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel, followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, @@ -542,6 +436,7 @@ private fun RenderScreen( baseUser, threadsViewModel, repliesViewModel, + mutualViewModel, followsFeedViewModel, followersFeedViewModel, zapFeedViewModel, @@ -561,6 +456,7 @@ private fun CreateAndRenderPages( baseUser: User, threadsViewModel: NostrUserProfileNewThreadsFeedViewModel, repliesViewModel: NostrUserProfileConversationsFeedViewModel, + mutualViewModel: NostrUserProfileMutualFeedViewModel, followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel, followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel, zapFeedViewModel: NostrUserProfileZapsFeedViewModel, @@ -580,14 +476,15 @@ private fun CreateAndRenderPages( when (page) { 0 -> TabNotesNewThreads(threadsViewModel, accountViewModel, nav) 1 -> TabNotesConversations(repliesViewModel, accountViewModel, nav) - 2 -> TabGallery(galleryFeedViewModel, accountViewModel, nav) - 3 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav) - 4 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav) - 5 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav) - 6 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav) - 7 -> TabFollowedTags(baseUser, accountViewModel, nav) - 8 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav) - 9 -> TabRelays(baseUser, accountViewModel, nav) + 2 -> TabMutualConversations(mutualViewModel, accountViewModel, nav) + 3 -> TabGallery(galleryFeedViewModel, accountViewModel, nav) + 4 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav) + 5 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav) + 6 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav) + 7 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav) + 8 -> TabFollowedTags(baseUser, accountViewModel, nav) + 9 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav) + 10 -> TabRelays(baseUser, accountViewModel, nav) } } @@ -610,7 +507,6 @@ fun UpdateThreadsAndRepliesWhenBlockUnblock( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun CreateAndRenderTabs( baseUser: User, @@ -622,6 +518,7 @@ private fun CreateAndRenderTabs( listOf<@Composable (() -> Unit)?>( { Text(text = stringRes(R.string.notes)) }, { Text(text = stringRes(R.string.replies)) }, + { Text(text = stringRes(R.string.mutual)) }, { Text(text = stringRes(R.string.gallery)) }, { FollowTabHeader(baseUser) }, { FollowersTabHeader(baseUser) }, @@ -640,1403 +537,3 @@ private fun CreateAndRenderTabs( ) } } - -@Composable -private fun RelaysTabHeader(baseUser: User) { - val userState by baseUser.live().relays.observeAsState() - val userRelaysBeingUsed = remember(userState) { userState?.user?.relaysBeingUsed?.size ?: "--" } - - val userStateRelayInfo by baseUser.live().relayInfo.observeAsState() - val userRelays = - remember(userStateRelayInfo) { - userStateRelayInfo - ?.user - ?.latestContactList - ?.relays() - ?.size ?: "--" - } - - Text(text = "$userRelaysBeingUsed / $userRelays ${stringRes(R.string.relays)}") -} - -@Composable -private fun ReportsTabHeader(baseUser: User) { - val userState by baseUser.live().reports.observeAsState() - var userReports by remember { mutableIntStateOf(0) } - - LaunchedEffect(key1 = userState) { - launch(Dispatchers.IO) { - val newSize = UserProfileReportsFeedFilter(baseUser).feed().size - - if (newSize != userReports) { - userReports = newSize - } - } - } - - Text(text = "$userReports ${stringRes(R.string.reports)}") -} - -@Composable -private fun FollowedTagsTabHeader(baseUser: User) { - val userState by baseUser.live().follows.observeAsState() - - val usertags by remember(baseUser) { - derivedStateOf { - userState?.user?.latestContactList?.countFollowTags() ?: 0 - } - } - - Text(text = "$usertags ${stringRes(R.string.followed_tags)}") -} - -@Composable -private fun BookmarkTabHeader(baseUser: User) { - val userState by baseUser.live().bookmarks.observeAsState() - - var userBookmarks by remember { mutableIntStateOf(0) } - - LaunchedEffect(key1 = userState) { - launch(Dispatchers.IO) { - val bookmarkList = userState?.user?.latestBookmarkList - - val newBookmarks = - ( - bookmarkList?.taggedEvents()?.count() - ?: 0 - ) + (bookmarkList?.taggedATags()?.count() ?: 0) - - if (newBookmarks != userBookmarks) { - userBookmarks = newBookmarks - } - } - } - - Text(text = "$userBookmarks ${stringRes(R.string.bookmarks)}") -} - -@Composable -private fun ZapTabHeader(baseUser: User) { - val userState by baseUser.live().zaps.observeAsState() - var zapAmount by remember { mutableStateOf(null) } - - LaunchedEffect(key1 = userState) { - launch(Dispatchers.Default) { - val tempAmount = baseUser.zappedAmount() - if (zapAmount != tempAmount) { - zapAmount = tempAmount - } - } - } - - Text(text = "${showAmountInteger(zapAmount)} ${stringRes(id = R.string.zaps)}") -} - -@Composable -private fun FollowersTabHeader(baseUser: User) { - val userState by baseUser.live().followers.observeAsState() - var followerCount by remember { mutableStateOf("--") } - - val text = stringRes(R.string.followers) - - LaunchedEffect(key1 = userState) { - launch(Dispatchers.IO) { - val newFollower = (userState?.user?.transientFollowerCount()?.toString() ?: "--") + " " + text - - if (followerCount != newFollower) { - followerCount = newFollower - } - } - } - - Text(text = followerCount) -} - -@Composable -private fun FollowTabHeader(baseUser: User) { - val userState by baseUser.live().follows.observeAsState() - var followCount by remember { mutableStateOf("--") } - - val text = stringRes(R.string.follows) - - LaunchedEffect(key1 = userState) { - launch(Dispatchers.IO) { - val newFollow = (userState?.user?.transientFollowCount()?.toString() ?: "--") + " " + text - - if (followCount != newFollow) { - followCount = newFollow - } - } - } - - Text(text = followCount) -} - -@Composable -private fun ProfileHeader( - baseUser: User, - appRecommendations: NostrUserAppRecommendationsFeedViewModel, - nav: INav, - accountViewModel: AccountViewModel, -) { - var popupExpanded by remember { mutableStateOf(false) } - var zoomImageDialogOpen by remember { mutableStateOf(false) } - - Box { - DrawBanner(baseUser, accountViewModel) - - Box( - modifier = - Modifier - .statusBarsPadding() - .padding(start = 10.dp, end = 10.dp, top = 10.dp) - .size(40.dp) - .align(Alignment.TopEnd), - ) { - Button( - modifier = - Modifier - .size(30.dp) - .align(Alignment.Center), - onClick = { popupExpanded = true }, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.background, - ), - contentPadding = ZeroPadding, - ) { - Icon( - tint = MaterialTheme.colorScheme.placeholderText, - imageVector = Icons.Default.MoreVert, - contentDescription = stringRes(R.string.more_options), - ) - - UserProfileDropDownMenu( - baseUser, - popupExpanded, - { popupExpanded = false }, - accountViewModel, - ) - } - } - - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp) - .padding(top = 100.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, - ) { - val clipboardManager = LocalClipboardManager.current - - ClickableUserPicture( - baseUser = baseUser, - accountViewModel = accountViewModel, - size = Size100dp, - modifier = MaterialTheme.colorScheme.userProfileBorderModifier, - onClick = { - if (baseUser.profilePicture() != null) { - zoomImageDialogOpen = true - } - }, - onLongClick = { - it.info?.picture?.let { it1 -> - clipboardManager.setText( - AnnotatedString(it1), - ) - } - }, - ) - - Spacer(Modifier.weight(1f)) - - Row( - modifier = - Modifier - .height(Size35dp) - .padding(bottom = 3.dp), - ) { - MessageButton(baseUser, accountViewModel, nav) - - ProfileActions(baseUser, accountViewModel, nav) - } - } - - DrawAdditionalInfo(baseUser, appRecommendations, accountViewModel, nav) - - HorizontalDivider(modifier = Modifier.padding(top = 6.dp)) - } - } - - val profilePic = baseUser.profilePicture() - if (zoomImageDialogOpen && profilePic != null) { - ZoomableImageDialog( - RichTextParser.parseImageOrVideo(profilePic), - onDismiss = { zoomImageDialogOpen = false }, - accountViewModel = accountViewModel, - ) - } -} - -@Composable -private fun ProfileActions( - baseUser: User, - accountViewModel: AccountViewModel, - nav: INav, -) { - val isMe by - remember(accountViewModel) { derivedStateOf { accountViewModel.userProfile() == baseUser } } - - if (isMe) { - EditButton(nav) - } - - WatchIsHiddenUser(baseUser, accountViewModel) { isHidden -> - if (isHidden) { - ShowUserButton { accountViewModel.showUser(baseUser.pubkeyHex) } - } else { - DisplayFollowUnfollowButton(baseUser, accountViewModel) - } - } -} - -@Composable -private fun DisplayFollowUnfollowButton( - baseUser: User, - accountViewModel: AccountViewModel, -) { - val isLoggedInFollowingUser by - accountViewModel.account - .userProfile() - .live() - .follows - .map { it.user.isFollowing(baseUser) } - .distinctUntilChanged() - .observeAsState(initial = accountViewModel.account.isFollowing(baseUser)) - - val isUserFollowingLoggedIn by - baseUser - .live() - .follows - .map { it.user.isFollowing(accountViewModel.account.userProfile()) } - .distinctUntilChanged() - .observeAsState(initial = baseUser.isFollowing(accountViewModel.account.userProfile())) - - if (isLoggedInFollowingUser) { - UnfollowButton { - if (!accountViewModel.isWriteable()) { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_unfollow, - ) - } else { - accountViewModel.unfollow(baseUser) - } - } - } else { - if (isUserFollowingLoggedIn) { - FollowButton(R.string.follow_back) { - if (!accountViewModel.isWriteable()) { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_follow, - ) - } else { - accountViewModel.follow(baseUser) - } - } - } else { - FollowButton(R.string.follow) { - if (!accountViewModel.isWriteable()) { - accountViewModel.toast( - R.string.read_only_user, - R.string.login_with_a_private_key_to_be_able_to_follow, - ) - } else { - accountViewModel.follow(baseUser) - } - } - } - } -} - -@Composable -fun WatchIsHiddenUser( - baseUser: User, - accountViewModel: AccountViewModel, - content: @Composable (Boolean) -> Unit, -) { - val isHidden by - accountViewModel.account.liveHiddenUsers - .map { - it.hiddenUsers.contains(baseUser.pubkeyHex) || it.spammers.contains(baseUser.pubkeyHex) - }.observeAsState(accountViewModel.account.isHidden(baseUser)) - - content(isHidden) -} - -fun getIdentityClaimIcon(identity: IdentityClaim): Int = - when (identity) { - is TwitterIdentity -> R.drawable.x - is TelegramIdentity -> R.drawable.telegram - is MastodonIdentity -> R.drawable.mastodon - is GitHubIdentity -> R.drawable.github - else -> R.drawable.github - } - -fun getIdentityClaimDescription(identity: IdentityClaim): Int = - when (identity) { - is TwitterIdentity -> R.string.twitter - is TelegramIdentity -> R.string.telegram - is MastodonIdentity -> R.string.mastodon - is GitHubIdentity -> R.string.github - else -> R.drawable.github - } - -@Composable -private fun DrawAdditionalInfo( - baseUser: User, - appRecommendations: NostrUserAppRecommendationsFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - val userState by baseUser.live().metadata.observeAsState() - val user = remember(userState) { userState?.user } ?: return - val tags = userState?.user?.info?.tags - - val uri = LocalUriHandler.current - val clipboardManager = LocalClipboardManager.current - - user.toBestDisplayName().let { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 7.dp)) { - CreateTextWithEmoji( - text = it, - tags = tags, - fontWeight = FontWeight.Bold, - fontSize = 25.sp, - ) - Spacer(StdHorzSpacer) - user.info?.pronouns?.let { - Text( - text = "($it)", - modifier = Modifier, - ) - Spacer(StdHorzSpacer) - } - - DrawPlayName(it) - } - } - - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = user.pubkeyDisplayHex(), - modifier = Modifier.padding(top = 1.dp, bottom = 1.dp), - color = MaterialTheme.colorScheme.placeholderText, - ) - - IconButton( - modifier = - Modifier - .size(25.dp) - .padding(start = 5.dp), - onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())) }, - ) { - Icon( - imageVector = Icons.Default.ContentCopy, - contentDescription = stringRes(id = R.string.copy_npub_to_clipboard), - modifier = Size15Modifier, - tint = MaterialTheme.colorScheme.placeholderText, - ) - } - - var dialogOpen by remember { mutableStateOf(false) } - - if (dialogOpen) { - ShowQRDialog( - user = user, - accountViewModel = accountViewModel, - onScan = { - dialogOpen = false - nav.nav(it) - }, - onClose = { dialogOpen = false }, - ) - } - - IconButton( - modifier = Size25Modifier, - onClick = { dialogOpen = true }, - ) { - Icon( - painter = painterResource(R.drawable.ic_qrcode), - contentDescription = stringRes(id = R.string.show_npub_as_a_qr_code), - modifier = Size15Modifier, - tint = MaterialTheme.colorScheme.placeholderText, - ) - } - } - - DisplayBadges(baseUser, accountViewModel, nav) - - DisplayNip05ProfileStatus(user, accountViewModel) - - val website = user.info?.website - if (!website.isNullOrEmpty()) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - tint = MaterialTheme.colorScheme.placeholderText, - imageVector = Icons.Default.Link, - contentDescription = stringRes(R.string.website), - modifier = Modifier.size(16.dp), - ) - - ClickableText( - text = AnnotatedString(website.removePrefix("https://")), - onClick = { - website.let { - runCatching { - if (it.contains("://")) { - uri.openUri(it) - } else { - uri.openUri("http://$it") - } - } - } - }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), - modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp), - ) - } - } - - val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() } - val pubkeyHex = remember { baseUser.pubkeyHex } - DisplayLNAddress(lud16, pubkeyHex, accountViewModel, nav) - - val identities = user.latestMetadata?.identityClaims() - if (!identities.isNullOrEmpty()) { - identities.forEach { identity: IdentityClaim -> - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - tint = Color.Unspecified, - painter = painterResource(id = getIdentityClaimIcon(identity)), - contentDescription = stringRes(getIdentityClaimDescription(identity)), - modifier = Modifier.size(16.dp), - ) - - ClickableText( - text = AnnotatedString(identity.identity), - onClick = { runCatching { uri.openUri(identity.toProofUrl()) } }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), - modifier = - Modifier - .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) - .weight(1f), - ) - } - } - } - - user.info?.about?.let { - Row( - modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), - ) { - val defaultBackground = MaterialTheme.colorScheme.background - val background = remember { mutableStateOf(defaultBackground) } - - TranslatableRichTextViewer( - content = it, - canPreview = false, - quotesLeft = 1, - tags = EmptyTagList, - backgroundColor = background, - id = it, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } - - DisplayAppRecommendations(appRecommendations, accountViewModel, nav) -} - -@Composable -fun DisplayLNAddress( - lud16: String?, - userHex: String, - accountViewModel: AccountViewModel, - nav: INav, -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - var zapExpanded by remember { mutableStateOf(false) } - - var showErrorMessageDialog by remember { mutableStateOf(null) } - - if (showErrorMessageDialog != null) { - ErrorMessageDialog( - title = stringRes(id = R.string.error_dialog_zap_error), - textContent = showErrorMessageDialog ?: "", - onClickStartMessage = { - scope.launch(Dispatchers.IO) { - val route = routeToMessage(userHex, showErrorMessageDialog, accountViewModel) - nav.nav(route) - } - }, - onDismiss = { showErrorMessageDialog = null }, - ) - } - - var showInfoMessageDialog by remember { mutableStateOf(null) } - if (showInfoMessageDialog != null) { - InformationDialog( - title = stringRes(context, R.string.payment_successful), - textContent = showInfoMessageDialog ?: "", - ) { - showInfoMessageDialog = null - } - } - - if (!lud16.isNullOrEmpty()) { - Row(verticalAlignment = Alignment.CenterVertically) { - LightningAddressIcon(modifier = Size16Modifier, tint = BitcoinOrange) - - ClickableText( - text = AnnotatedString(lud16), - onClick = { zapExpanded = !zapExpanded }, - style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), - modifier = - Modifier - .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) - .weight(1f), - ) - } - - if (zapExpanded) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(vertical = 5.dp), - ) { - InvoiceRequestCard( - lud16, - userHex, - accountViewModel, - onSuccess = { - zapExpanded = false - // pay directly - if (accountViewModel.account.hasWalletConnectSetup()) { - accountViewModel.sendZapPaymentRequestFor(it, null, onSent = {}) { response -> - if (response is PayInvoiceSuccessResponse) { - showInfoMessageDialog = stringRes(context, R.string.payment_successful) - } else if (response is PayInvoiceErrorResponse) { - showErrorMessageDialog = - response.error?.message - ?: response.error?.code?.toString() - ?: stringRes(context, R.string.error_parsing_error_message) - } - } - } else { - payViaIntent(it, context, { zapExpanded = false }, { showErrorMessageDialog = it }) - } - }, - onClose = { zapExpanded = false }, - onError = { title, message -> accountViewModel.toast(title, message) }, - ) - } - } - } -} - -@Composable -@OptIn(ExperimentalLayoutApi::class) -private fun DisplayAppRecommendations( - appRecommendations: NostrUserAppRecommendationsFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - val feedState by appRecommendations.feedState.feedContent.collectAsStateWithLifecycle() - - LaunchedEffect(key1 = Unit) { appRecommendations.invalidateData() } - - CrossfadeIfEnabled( - targetState = feedState, - animationSpec = tween(durationMillis = 100), - accountViewModel = accountViewModel, - ) { state -> - when (state) { - is FeedState.Loaded -> { - Column { - Text(stringRes(id = R.string.recommended_apps)) - - Recommends(state, nav) - } - } - else -> {} - } - } -} - -@Composable -@OptIn(ExperimentalLayoutApi::class) -private fun Recommends( - loaded: FeedState.Loaded, - nav: INav, -) { - val items by loaded.feed.collectAsStateWithLifecycle() - FlowRow( - verticalArrangement = Arrangement.Center, - modifier = Modifier.padding(vertical = 5.dp), - ) { - items.list.forEach { app -> WatchApp(app, nav) } - } -} - -@Composable -private fun WatchApp( - baseApp: Note, - nav: INav, -) { - val appState by baseApp.live().metadata.observeAsState() - - var appLogo by remember(baseApp) { mutableStateOf(null) } - var appName by remember(baseApp) { mutableStateOf(null) } - - LaunchedEffect(key1 = appState) { - withContext(Dispatchers.Default) { - (appState?.note?.event as? AppDefinitionEvent)?.appMetaData()?.let { metaData -> - metaData.picture?.ifBlank { null }?.let { newLogo -> - if (newLogo != appLogo) appLogo = newLogo - } - metaData.name?.ifBlank { null }?.let { newName -> - if (newName != appName) appName = newName - } - } - } - } - - appLogo?.let { - Box( - remember { - Modifier - .size(Size35dp) - .clickable { nav.nav("Note/${baseApp.idHex}") } - }, - ) { - AsyncImage( - model = appLogo, - contentDescription = appName, - modifier = - remember { - Modifier - .size(Size35dp) - .clip(shape = CircleShape) - }, - ) - } - } -} - -@Composable -private fun DisplayBadges( - baseUser: User, - accountViewModel: AccountViewModel, - nav: INav, -) { - LoadAddressableNote( - BadgeProfilesEvent.createAddress(baseUser.pubkeyHex), - accountViewModel, - ) { note -> - if (note != null) { - WatchAndRenderBadgeList( - note = note, - loadProfilePicture = accountViewModel.settings.showProfilePictures.value, - loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, - nav = nav, - ) - } - } -} - -@Composable -private fun WatchAndRenderBadgeList( - note: AddressableNote, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - nav: INav, -) { - val badgeList by - note - .live() - .metadata - .map { (it.note.event as? BadgeProfilesEvent)?.badgeAwardEvents()?.toImmutableList() } - .distinctUntilChanged() - .observeAsState() - - badgeList?.let { list -> RenderBadgeList(list, loadProfilePicture, loadRobohash, nav) } -} - -@Composable -@OptIn(ExperimentalLayoutApi::class) -private fun RenderBadgeList( - list: ImmutableList, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - nav: INav, -) { - FlowRow( - verticalArrangement = Arrangement.Center, - modifier = Modifier.padding(vertical = 5.dp), - ) { - list.forEach { badgeAwardEvent -> LoadAndRenderBadge(badgeAwardEvent, loadProfilePicture, loadRobohash, nav) } - } -} - -@Composable -private fun LoadAndRenderBadge( - badgeAwardEvent: ETag, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - nav: INav, -) { - var baseNote by remember(badgeAwardEvent) { mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEvent)) } - - LaunchedEffect(key1 = badgeAwardEvent) { - if (baseNote == null) { - withContext(Dispatchers.IO) { - baseNote = LocalCache.checkGetOrCreateNote(badgeAwardEvent) - } - } - } - - baseNote?.let { ObserveAndRenderBadge(it, loadProfilePicture, loadRobohash, nav) } -} - -@Composable -private fun ObserveAndRenderBadge( - it: Note, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - nav: INav, -) { - val badgeAwardState by it.live().metadata.observeAsState() - val baseBadgeDefinition by - remember(badgeAwardState) { derivedStateOf { badgeAwardState?.note?.replyTo?.firstOrNull() } } - - baseBadgeDefinition?.let { BadgeThumb(it, loadProfilePicture, loadRobohash, nav, Size35dp) } -} - -@Composable -fun BadgeThumb( - note: Note, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - nav: INav, - size: Dp, - pictureModifier: Modifier = Modifier, -) { - BadgeThumb(note, loadProfilePicture, loadRobohash, size, pictureModifier) { nav.nav("Note/${note.idHex}") } -} - -@Composable -fun BadgeThumb( - baseNote: Note, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - size: Dp, - pictureModifier: Modifier = Modifier, - onClick: ((String) -> Unit)? = null, -) { - Box( - remember { - Modifier - .width(size) - .height(size) - }, - ) { - WatchAndRenderBadgeImage(baseNote, loadProfilePicture, loadRobohash, size, pictureModifier, onClick) - } -} - -@Composable -private fun WatchAndRenderBadgeImage( - baseNote: Note, - loadProfilePicture: Boolean, - loadRobohash: Boolean, - size: Dp, - pictureModifier: Modifier, - onClick: ((String) -> Unit)?, -) { - val noteState by baseNote.live().metadata.observeAsState() - val eventId = remember(noteState) { noteState?.note?.idHex } ?: return - val image by - remember(noteState) { - derivedStateOf { - val event = noteState?.note?.event as? BadgeDefinitionEvent - event?.thumb()?.ifBlank { null } ?: event?.image()?.ifBlank { null } - } - } - - if (image == null) { - RobohashAsyncImage( - robot = "authornotfound", - contentDescription = stringRes(R.string.unknown_author), - modifier = - remember { - pictureModifier - .width(size) - .height(size) - }, - loadRobohash = loadRobohash, - ) - } else { - RobohashFallbackAsyncImage( - robot = eventId, - model = image!!, - contentDescription = stringRes(id = R.string.profile_image), - modifier = - remember { - pictureModifier - .width(size) - .height(size) - .clip(shape = CutCornerShape(20)) - .run { - if (onClick != null) { - this.clickable(onClick = { onClick(eventId) }) - } else { - this - } - } - }, - loadProfilePicture = loadProfilePicture, - loadRobohash = loadRobohash, - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun DrawBanner( - baseUser: User, - accountViewModel: AccountViewModel, -) { - val userState by baseUser.live().metadata.observeAsState() - val banner = remember(userState) { userState?.user?.info?.banner } - - val clipboardManager = LocalClipboardManager.current - var zoomImageDialogOpen by remember { mutableStateOf(false) } - - if (!banner.isNullOrBlank()) { - AsyncImage( - model = banner, - contentDescription = stringRes(id = R.string.profile_image), - contentScale = ContentScale.FillWidth, - placeholder = painterResource(R.drawable.profile_banner), - modifier = - Modifier - .fillMaxWidth() - .height(150.dp) - .combinedClickable( - onClick = { zoomImageDialogOpen = true }, - onLongClick = { clipboardManager.setText(AnnotatedString(banner)) }, - ), - ) - - if (zoomImageDialogOpen) { - ZoomableImageDialog( - imageUrl = RichTextParser.parseImageOrVideo(banner), - onDismiss = { zoomImageDialogOpen = false }, - accountViewModel = accountViewModel, - ) - } - } else { - Image( - painter = painterResource(R.drawable.profile_banner), - contentDescription = stringRes(id = R.string.profile_banner), - contentScale = ContentScale.FillWidth, - modifier = - Modifier - .fillMaxWidth() - .height(150.dp), - ) - } -} - -@Composable -fun TabNotesNewThreads( - feedViewModel: NostrUserProfileNewThreadsFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - Column(Modifier.fillMaxHeight()) { - RefresheableFeedView( - feedViewModel, - null, - enablePullRefresh = false, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} - -@Composable -fun TabNotesConversations( - feedViewModel: NostrUserProfileConversationsFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - Column(Modifier.fillMaxHeight()) { - RefresheableFeedView( - feedViewModel, - null, - enablePullRefresh = false, - accountViewModel = accountViewModel, - nav = nav, - ) - } -} - -@Composable -fun TabGallery( - feedViewModel: NostrUserProfileGalleryFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - LaunchedEffect(Unit) { feedViewModel.invalidateData() } - - Column(Modifier.fillMaxHeight()) { - SaveableGridFeedState(feedViewModel, scrollStateKey = ScrollStateKeys.PROFILE_GALLERY) { listState -> - RenderGalleryFeed( - feedViewModel, - listState, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} - -@Composable -fun TabFollowedTags( - baseUser: User, - account: AccountViewModel, - nav: INav, -) { - val items = - remember(baseUser) { - baseUser.latestContactList?.unverifiedFollowTagSet() - } - - Column( - Modifier - .fillMaxHeight() - .padding(vertical = 0.dp), - ) { - items?.let { - LazyColumn { - itemsIndexed(items) { index, hashtag -> - HashtagHeader( - tag = hashtag, - account = account, - onClick = { nav.nav("Hashtag/$hashtag") }, - ) - HorizontalDivider( - thickness = DividerThickness, - ) - } - } - } - } -} - -@Composable -fun TabBookmarks( - feedViewModel: NostrUserProfileBookmarksFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - LaunchedEffect(Unit) { feedViewModel.invalidateData() } - - Column(Modifier.fillMaxHeight()) { - Column( - modifier = Modifier.padding(vertical = 0.dp), - ) { - RefresheableFeedView( - feedViewModel, - null, - enablePullRefresh = false, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} - -@Composable -fun TabFollows( - baseUser: User, - feedViewModel: UserFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - WatchFollowChanges(baseUser, feedViewModel) - - Column(Modifier.fillMaxHeight()) { - RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false) - } -} - -@Composable -fun TabFollowers( - baseUser: User, - feedViewModel: UserFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - WatchFollowerChanges(baseUser, feedViewModel) - - Column(Modifier.fillMaxHeight()) { - RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false) - } -} - -@Composable -private fun WatchFollowChanges( - baseUser: User, - feedViewModel: UserFeedViewModel, -) { - val userState by baseUser.live().follows.observeAsState() - - LaunchedEffect(userState) { feedViewModel.invalidateData() } -} - -@Composable -private fun WatchFollowerChanges( - baseUser: User, - feedViewModel: UserFeedViewModel, -) { - val userState by baseUser.live().followers.observeAsState() - - LaunchedEffect(userState) { feedViewModel.invalidateData() } -} - -@Composable -fun TabReceivedZaps( - baseUser: User, - zapFeedViewModel: NostrUserProfileZapsFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - WatchZapsAndUpdateFeed(baseUser, zapFeedViewModel) - - Column(Modifier.fillMaxHeight()) { - LnZapFeedView(zapFeedViewModel, accountViewModel, nav) - } -} - -@Composable -private fun WatchZapsAndUpdateFeed( - baseUser: User, - feedViewModel: NostrUserProfileZapsFeedViewModel, -) { - val userState by baseUser.live().zaps.observeAsState() - - LaunchedEffect(userState) { feedViewModel.invalidateData() } -} - -@Composable -fun TabReports( - baseUser: User, - feedViewModel: NostrUserProfileReportFeedViewModel, - accountViewModel: AccountViewModel, - nav: INav, -) { - WatchReportsAndUpdateFeed(baseUser, feedViewModel) - - Column(Modifier.fillMaxHeight()) { - Column { - RefresheableFeedView( - feedViewModel, - null, - enablePullRefresh = false, - accountViewModel = accountViewModel, - nav = nav, - ) - } - } -} - -@Composable -private fun WatchReportsAndUpdateFeed( - baseUser: User, - feedViewModel: NostrUserProfileReportFeedViewModel, -) { - val userState by baseUser.live().reports.observeAsState() - LaunchedEffect(userState) { feedViewModel.invalidateData() } -} - -@Composable -fun TabRelays( - user: User, - accountViewModel: AccountViewModel, - nav: INav, -) { - val feedViewModel: RelayFeedViewModel = viewModel() - - val lifeCycleOwner = LocalLifecycleOwner.current - - DisposableEffect(user) { - feedViewModel.subscribeTo(user) - onDispose { feedViewModel.unsubscribeTo(user) } - } - - DisposableEffect(lifeCycleOwner) { - val observer = - LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_RESUME) { - println("Profile Relay Start") - feedViewModel.subscribeTo(user) - } - if (event == Lifecycle.Event.ON_PAUSE) { - println("Profile Relay Stop") - feedViewModel.unsubscribeTo(user) - } - } - - lifeCycleOwner.lifecycle.addObserver(observer) - onDispose { - lifeCycleOwner.lifecycle.removeObserver(observer) - println("Profile Relay Dispose") - feedViewModel.unsubscribeTo(user) - } - } - - Column(Modifier.fillMaxHeight()) { - RelayFeedView(feedViewModel, accountViewModel, enablePullRefresh = false, nav = nav) - } -} - -@Composable -private fun MessageButton( - user: User, - accountViewModel: AccountViewModel, - nav: INav, -) { - val scope = rememberCoroutineScope() - - Button( - modifier = - Modifier - .padding(horizontal = 3.dp) - .width(50.dp), - onClick = { - scope.launch(Dispatchers.IO) { accountViewModel.createChatRoomFor(user) { nav.nav("Room/$it") } } - }, - contentPadding = ZeroPadding, - ) { - Icon( - painter = painterResource(R.drawable.ic_dm), - stringRes(R.string.send_a_direct_message), - modifier = Modifier.size(20.dp), - tint = Color.White, - ) - } -} - -@Composable -private fun EditButton(nav: INav) { - InnerEditButton { nav.nav(Route.EditProfile.route) } -} - -@Preview -@Composable -private fun InnerEditButtonPreview() { - InnerEditButton {} -} - -@Composable -private fun InnerEditButton(onClick: () -> Unit) { - Button( - modifier = - Modifier - .padding(horizontal = 3.dp) - .width(50.dp), - onClick = onClick, - contentPadding = ZeroPadding, - ) { - Icon( - tint = Color.White, - imageVector = Icons.Default.EditNote, - contentDescription = stringRes(R.string.edits_the_user_s_metadata), - ) - } -} - -@Composable -fun UnfollowButton(onClick: () -> Unit) { - Button( - modifier = Modifier.padding(horizontal = 3.dp), - onClick = onClick, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - contentPadding = ButtonPadding, - ) { - Text(text = stringRes(R.string.unfollow), color = Color.White) - } -} - -@Composable -fun FollowButton( - text: Int = R.string.follow, - onClick: () -> Unit, -) { - Button( - modifier = Modifier.padding(start = 3.dp), - onClick = onClick, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - contentPadding = ButtonPadding, - ) { - Text(text = stringRes(text), color = Color.White, textAlign = TextAlign.Center) - } -} - -@Composable -fun ShowUserButton(onClick: () -> Unit) { - Button( - modifier = Modifier.padding(start = 3.dp), - onClick = onClick, - shape = ButtonBorder, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - contentPadding = ButtonPadding, - ) { - Text(text = stringRes(R.string.unblock), color = Color.White) - } -} - -@Composable -fun UserProfileDropDownMenu( - user: User, - popupExpanded: Boolean, - onDismiss: () -> Unit, - accountViewModel: AccountViewModel, -) { - DropdownMenu( - expanded = popupExpanded, - onDismissRequest = onDismiss, - ) { - val clipboardManager = LocalClipboardManager.current - - DropdownMenuItem( - text = { Text(stringRes(R.string.copy_user_id)) }, - onClick = { - clipboardManager.setText(AnnotatedString(user.pubkeyNpub())) - onDismiss() - }, - ) - - val actContext = LocalContext.current - - DropdownMenuItem( - text = { Text(stringRes(R.string.quick_action_share)) }, - onClick = { - val sendIntent = - Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra( - Intent.EXTRA_TEXT, - externalLinkForUser(user), - ) - putExtra( - Intent.EXTRA_TITLE, - stringRes(actContext, R.string.quick_action_share_browser_link), - ) - } - - val shareIntent = - Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share)) - ContextCompat.startActivity(actContext, shareIntent, null) - onDismiss() - }, - ) - - if (accountViewModel.userProfile() != user) { - HorizontalDivider(thickness = DividerThickness) - if (accountViewModel.account.isHidden(user)) { - DropdownMenuItem( - text = { Text(stringRes(R.string.unblock_user)) }, - onClick = { - accountViewModel.show(user) - onDismiss() - }, - ) - } else { - DropdownMenuItem( - text = { Text(stringRes(id = R.string.block_hide_user)) }, - onClick = { - accountViewModel.hide(user) - onDismiss() - }, - ) - } - HorizontalDivider(thickness = DividerThickness) - DropdownMenuItem( - text = { Text(stringRes(id = R.string.report_spam_scam)) }, - onClick = { - accountViewModel.report(user, ReportEvent.ReportType.SPAM) - onDismiss() - }, - ) - DropdownMenuItem( - text = { Text(stringRes(R.string.report_hateful_speech)) }, - onClick = { - accountViewModel.report(user, ReportEvent.ReportType.PROFANITY) - onDismiss() - }, - ) - DropdownMenuItem( - text = { Text(stringRes(id = R.string.report_impersonation)) }, - onClick = { - accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION) - onDismiss() - }, - ) - DropdownMenuItem( - text = { Text(stringRes(R.string.report_nudity_porn)) }, - onClick = { - accountViewModel.report(user, ReportEvent.ReportType.NUDITY) - onDismiss() - }, - ) - DropdownMenuItem( - text = { Text(stringRes(id = R.string.report_illegal_behaviour)) }, - onClick = { - accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL) - onDismiss() - }, - ) - DropdownMenuItem( - text = { Text(stringRes(id = R.string.report_malware)) }, - onClick = { - accountViewModel.report(user, ReportEvent.ReportType.MALWARE) - onDismiss() - }, - ) - } - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt new file mode 100644 index 000000000..bbb4f51e9 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/BookmarkTabHeader.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks + +import androidx.compose.material3.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.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun BookmarkTabHeader(baseUser: User) { + val userState by baseUser.live().bookmarks.observeAsState() + + var userBookmarks by remember { mutableIntStateOf(0) } + + LaunchedEffect(key1 = userState) { + launch(Dispatchers.IO) { + val newBookmarks = userState?.user?.latestBookmarkList?.countBookmarks() ?: 0 + + if (newBookmarks != userBookmarks) { + userBookmarks = newBookmarks + } + } + } + + Text(text = "$userBookmarks ${stringRes(R.string.bookmarks)}") +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/NostrUserProfileBookmarksFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/NostrUserProfileBookmarksFeedViewModel.kt new file mode 100644 index 000000000..e88e52663 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/NostrUserProfileBookmarksFeedViewModel.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter +import com.vitorpamplona.amethyst.ui.screen.FeedViewModel + +class NostrUserProfileBookmarksFeedViewModel( + val user: User, + val account: Account, +) : FeedViewModel(UserProfileBookmarksFeedFilter(user, account)) { + class Factory( + val user: User, + val account: Account, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileBookmarksFeedViewModel = + NostrUserProfileBookmarksFeedViewModel(user, account) + as NostrUserProfileBookmarksFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/TabBookmarks.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/TabBookmarks.kt new file mode 100644 index 000000000..5a43e0eff --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/bookmarks/TabBookmarks.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabBookmarks( + feedViewModel: NostrUserProfileBookmarksFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + LaunchedEffect(Unit) { feedViewModel.invalidateData() } + + Column(Modifier.fillMaxHeight()) { + Column( + modifier = Modifier.padding(vertical = 0.dp), + ) { + RefresheableFeedView( + feedViewModel, + null, + enablePullRefresh = false, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/NostrUserProfileConversationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/NostrUserProfileConversationsFeedViewModel.kt new file mode 100644 index 000000000..0e27d9fef --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/NostrUserProfileConversationsFeedViewModel.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter +import com.vitorpamplona.amethyst.ui.screen.FeedViewModel + +class NostrUserProfileConversationsFeedViewModel( + val user: User, + val account: Account, +) : FeedViewModel(UserProfileConversationsFeedFilter(user, account)) { + class Factory( + val user: User, + val account: Account, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileConversationsFeedViewModel = + NostrUserProfileConversationsFeedViewModel(user, account) + as NostrUserProfileConversationsFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/TabNotesConversations.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/TabNotesConversations.kt new file mode 100644 index 000000000..159415840 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/conversations/TabNotesConversations.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.conversations + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabNotesConversations( + feedViewModel: NostrUserProfileConversationsFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + Column(Modifier.fillMaxHeight()) { + RefresheableFeedView( + feedViewModel, + null, + enablePullRefresh = false, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt new file mode 100644 index 000000000..082576223 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/FollowersTabHeader.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers + +import androidx.compose.material3.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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun FollowersTabHeader(baseUser: User) { + val userState by baseUser.live().followers.observeAsState() + var followerCount by remember { mutableStateOf("--") } + + val text = stringRes(R.string.followers) + + LaunchedEffect(key1 = userState) { + launch(Dispatchers.IO) { + val newFollower = (userState?.user?.transientFollowerCount()?.toString() ?: "--") + " " + text + + if (followerCount != newFollower) { + followerCount = newFollower + } + } + } + + Text(text = followerCount) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/NostrUserProfileFollowersUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/NostrUserProfileFollowersUserFeedViewModel.kt new file mode 100644 index 000000000..2660475ed --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/NostrUserProfileFollowersUserFeedViewModel.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter +import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel + +class NostrUserProfileFollowersUserFeedViewModel( + val user: User, + val account: Account, +) : UserFeedViewModel(UserProfileFollowersFeedFilter(user, account)) { + class Factory( + val user: User, + val account: Account, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileFollowersUserFeedViewModel = + NostrUserProfileFollowersUserFeedViewModel(user, account) + as NostrUserProfileFollowersUserFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt new file mode 100644 index 000000000..2d9425789 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/followers/TabFollowers.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.followers + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView +import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabFollowers( + baseUser: User, + feedViewModel: UserFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + WatchFollowerChanges(baseUser, feedViewModel) + + Column(Modifier.fillMaxHeight()) { + RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false) + } +} + +@Composable +private fun WatchFollowerChanges( + baseUser: User, + feedViewModel: UserFeedViewModel, +) { + val userState by baseUser.live().followers.observeAsState() + + LaunchedEffect(userState) { feedViewModel.invalidateData() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt new file mode 100644 index 000000000..c1c324ffa --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/FollowTabHeader.kt @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows + +import androidx.compose.material3.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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun FollowTabHeader(baseUser: User) { + val userState by baseUser.live().follows.observeAsState() + var followCount by remember { mutableStateOf("--") } + + val text = stringRes(R.string.follows) + + LaunchedEffect(key1 = userState) { + launch(Dispatchers.IO) { + val newFollow = (userState?.user?.transientFollowCount()?.toString() ?: "--") + " " + text + + if (followCount != newFollow) { + followCount = newFollow + } + } + } + + Text(text = followCount) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/NostrUserProfileFollowsUserFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/NostrUserProfileFollowsUserFeedViewModel.kt new file mode 100644 index 000000000..2776ad572 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/NostrUserProfileFollowsUserFeedViewModel.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter +import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel + +class NostrUserProfileFollowsUserFeedViewModel( + val user: User, + val account: Account, +) : UserFeedViewModel(UserProfileFollowsFeedFilter(user, account)) { + class Factory( + val user: User, + val account: Account, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileFollowsUserFeedViewModel = + NostrUserProfileFollowsUserFeedViewModel(user, account) + as NostrUserProfileFollowsUserFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt new file mode 100644 index 000000000..77a059efe --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/follows/TabFollows.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.follows + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView +import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabFollows( + baseUser: User, + feedViewModel: UserFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + WatchFollowChanges(baseUser, feedViewModel) + + Column(Modifier.fillMaxHeight()) { + RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav, enablePullRefresh = false) + } +} + +@Composable +private fun WatchFollowChanges( + baseUser: User, + feedViewModel: UserFeedViewModel, +) { + val userState by baseUser.live().follows.observeAsState() + + LaunchedEffect(userState) { feedViewModel.invalidateData() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/NostrUserProfileGalleryFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/NostrUserProfileGalleryFeedViewModel.kt new file mode 100644 index 000000000..e24d2b2f3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/NostrUserProfileGalleryFeedViewModel.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileGalleryFeedFilter +import com.vitorpamplona.amethyst.ui.screen.FeedViewModel + +class NostrUserProfileGalleryFeedViewModel( + val user: User, + val account: Account, +) : FeedViewModel(UserProfileGalleryFeedFilter(user, account)) { + class Factory( + val user: User, + val account: Account, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileGalleryFeedViewModel = + NostrUserProfileGalleryFeedViewModel(user, account) + as NostrUserProfileGalleryFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/TabGallery.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/TabGallery.kt new file mode 100644 index 000000000..613c1360a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/gallery/TabGallery.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.ui.feeds.ScrollStateKeys +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabGallery( + feedViewModel: NostrUserProfileGalleryFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + LaunchedEffect(Unit) { feedViewModel.invalidateData() } + + Column(Modifier.fillMaxHeight()) { + SaveableGridFeedState(feedViewModel, scrollStateKey = ScrollStateKeys.PROFILE_GALLERY) { listState -> + RenderGalleryFeed( + feedViewModel, + listState, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt new file mode 100644 index 000000000..301a5262e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/FollowedTagsTabHeader.kt @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.stringRes + +@Composable +fun FollowedTagsTabHeader(baseUser: User) { + val userState by baseUser.live().follows.observeAsState() + + val usertags by remember(baseUser) { + derivedStateOf { + userState?.user?.latestContactList?.countFollowTags() ?: 0 + } + } + + Text(text = "$usertags ${stringRes(R.string.followed_tags)}") +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt new file mode 100644 index 000000000..b2d8720ba --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/hashtags/TabFollowedTags.kt @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.hashtags + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagHeader +import com.vitorpamplona.amethyst.ui.theme.DividerThickness + +@Composable +fun TabFollowedTags( + baseUser: User, + account: AccountViewModel, + nav: INav, +) { + val items = + remember(baseUser) { + baseUser.latestContactList?.unverifiedFollowTagSet() + } + + Column( + Modifier + .fillMaxHeight() + .padding(vertical = 0.dp), + ) { + items?.let { + LazyColumn { + itemsIndexed(items) { index, hashtag -> + HashtagHeader( + tag = hashtag, + account = account, + onClick = { nav.nav("Hashtag/$hashtag") }, + ) + HorizontalDivider( + thickness = DividerThickness, + ) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt new file mode 100644 index 000000000..9fe7176d2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayFollowUnfollowButton.kt @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.FollowButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.UnfollowButton + +@Composable +fun DisplayFollowUnfollowButton( + baseUser: User, + accountViewModel: AccountViewModel, +) { + val isLoggedInFollowingUser by + accountViewModel.account + .userProfile() + .live() + .follows + .map { it.user.isFollowing(baseUser) } + .distinctUntilChanged() + .observeAsState(initial = accountViewModel.account.isFollowing(baseUser)) + + val isUserFollowingLoggedIn by + baseUser + .live() + .follows + .map { it.user.isFollowing(accountViewModel.account.userProfile()) } + .distinctUntilChanged() + .observeAsState(initial = baseUser.isFollowing(accountViewModel.account.userProfile())) + + if (isLoggedInFollowingUser) { + UnfollowButton { + if (!accountViewModel.isWriteable()) { + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_unfollow, + ) + } else { + accountViewModel.unfollow(baseUser) + } + } + } else { + if (isUserFollowingLoggedIn) { + FollowButton(R.string.follow_back) { + if (!accountViewModel.isWriteable()) { + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow, + ) + } else { + accountViewModel.follow(baseUser) + } + } + } else { + FollowButton(R.string.follow) { + if (!accountViewModel.isWriteable()) { + accountViewModel.toast( + R.string.read_only_user, + R.string.login_with_a_private_key_to_be_able_to_follow, + ) + } else { + accountViewModel.follow(baseUser) + } + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt new file mode 100644 index 000000000..b34434435 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DisplayLNAddress.kt @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.InformationDialog +import com.vitorpamplona.amethyst.ui.components.ClickableText +import com.vitorpamplona.amethyst.ui.components.InvoiceRequestCard +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.routeToMessage +import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog +import com.vitorpamplona.amethyst.ui.note.LightningAddressIcon +import com.vitorpamplona.amethyst.ui.note.payViaIntent +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange +import com.vitorpamplona.amethyst.ui.theme.Size16Modifier +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse +import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun DisplayLNAddress( + lud16: String?, + userHex: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + var zapExpanded by remember { mutableStateOf(false) } + + var showErrorMessageDialog by remember { mutableStateOf(null) } + + if (showErrorMessageDialog != null) { + ErrorMessageDialog( + title = stringRes(id = R.string.error_dialog_zap_error), + textContent = showErrorMessageDialog ?: "", + onClickStartMessage = { + scope.launch(Dispatchers.IO) { + val route = routeToMessage(userHex, showErrorMessageDialog, accountViewModel) + nav.nav(route) + } + }, + onDismiss = { showErrorMessageDialog = null }, + ) + } + + var showInfoMessageDialog by remember { mutableStateOf(null) } + if (showInfoMessageDialog != null) { + InformationDialog( + title = stringRes(context, R.string.payment_successful), + textContent = showInfoMessageDialog ?: "", + ) { + showInfoMessageDialog = null + } + } + + if (!lud16.isNullOrEmpty()) { + Row(verticalAlignment = Alignment.CenterVertically) { + LightningAddressIcon(modifier = Size16Modifier, tint = BitcoinOrange) + + ClickableText( + text = AnnotatedString(lud16), + onClick = { zapExpanded = !zapExpanded }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), + modifier = + Modifier + .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) + .weight(1f), + ) + } + + if (zapExpanded) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 5.dp), + ) { + InvoiceRequestCard( + lud16, + userHex, + accountViewModel, + onSuccess = { + zapExpanded = false + // pay directly + if (accountViewModel.account.hasWalletConnectSetup()) { + accountViewModel.sendZapPaymentRequestFor(it, null, onSent = {}) { response -> + if (response is PayInvoiceSuccessResponse) { + showInfoMessageDialog = stringRes(context, R.string.payment_successful) + } else if (response is PayInvoiceErrorResponse) { + showErrorMessageDialog = + response.error?.message + ?: response.error?.code?.toString() + ?: stringRes(context, R.string.error_parsing_error_message) + } + } + } else { + payViaIntent(it, context, { zapExpanded = false }, { showErrorMessageDialog = it }) + } + }, + onClose = { zapExpanded = false }, + onError = { title, message -> accountViewModel.toast(title, message) }, + ) + } + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt new file mode 100644 index 000000000..a3d1fbba2 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -0,0 +1,260 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.Link +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.ClickableText +import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji +import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus +import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.DrawPlayName +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.NostrUserAppRecommendationsFeedViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges.DisplayBadges +import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ShowQRDialog +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size15Modifier +import com.vitorpamplona.amethyst.ui.theme.Size25Modifier +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList +import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity +import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaim +import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity +import com.vitorpamplona.quartz.nip39ExtIdentities.TelegramIdentity +import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity +import com.vitorpamplona.quartz.nip39ExtIdentities.identityClaims + +@Composable +fun DrawAdditionalInfo( + baseUser: User, + appRecommendations: NostrUserAppRecommendationsFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + val userState by baseUser.live().metadata.observeAsState() + val user = remember(userState) { userState?.user } ?: return + val tags = userState?.user?.info?.tags + + val uri = LocalUriHandler.current + val clipboardManager = LocalClipboardManager.current + + user.toBestDisplayName().let { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 7.dp)) { + CreateTextWithEmoji( + text = it, + tags = tags, + fontWeight = FontWeight.Bold, + fontSize = 25.sp, + ) + Spacer(StdHorzSpacer) + user.info?.pronouns?.let { + Text( + text = "($it)", + ) + Spacer(StdHorzSpacer) + } + + DrawPlayName(it) + } + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = user.pubkeyDisplayHex(), + modifier = Modifier.padding(top = 1.dp, bottom = 1.dp), + color = MaterialTheme.colorScheme.placeholderText, + ) + + IconButton( + modifier = + Modifier + .size(25.dp) + .padding(start = 5.dp), + onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())) }, + ) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = stringRes(id = R.string.copy_npub_to_clipboard), + modifier = Size15Modifier, + tint = MaterialTheme.colorScheme.placeholderText, + ) + } + + var dialogOpen by remember { mutableStateOf(false) } + + if (dialogOpen) { + ShowQRDialog( + user = user, + accountViewModel = accountViewModel, + onScan = { + dialogOpen = false + nav.nav(it) + }, + onClose = { dialogOpen = false }, + ) + } + + IconButton( + modifier = Size25Modifier, + onClick = { dialogOpen = true }, + ) { + Icon( + painter = painterResource(R.drawable.ic_qrcode), + contentDescription = stringRes(id = R.string.show_npub_as_a_qr_code), + modifier = Size15Modifier, + tint = MaterialTheme.colorScheme.placeholderText, + ) + } + } + + DisplayBadges(baseUser, accountViewModel, nav) + + DisplayNip05ProfileStatus(user, accountViewModel) + + val website = user.info?.website + if (!website.isNullOrEmpty()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + tint = MaterialTheme.colorScheme.placeholderText, + imageVector = Icons.Default.Link, + contentDescription = stringRes(R.string.website), + modifier = Modifier.size(16.dp), + ) + + ClickableText( + text = AnnotatedString(website.removePrefix("https://")), + onClick = { + website.let { + runCatching { + if (it.contains("://")) { + uri.openUri(it) + } else { + uri.openUri("http://$it") + } + } + } + }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), + modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp), + ) + } + } + + val lud16 = remember(userState) { user.info?.lud16?.trim() ?: user.info?.lud06?.trim() } + val pubkeyHex = remember { baseUser.pubkeyHex } + DisplayLNAddress(lud16, pubkeyHex, accountViewModel, nav) + + val identities = user.latestMetadata?.identityClaims() + if (!identities.isNullOrEmpty()) { + identities.forEach { identity: IdentityClaim -> + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + tint = Color.Unspecified, + painter = painterResource(id = getIdentityClaimIcon(identity)), + contentDescription = stringRes(getIdentityClaimDescription(identity)), + modifier = Modifier.size(16.dp), + ) + + ClickableText( + text = AnnotatedString(identity.identity), + onClick = { runCatching { uri.openUri(identity.toProofUrl()) } }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), + modifier = + Modifier + .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) + .weight(1f), + ) + } + } + } + + user.info?.about?.let { + Row( + modifier = Modifier.padding(top = 5.dp, bottom = 5.dp), + ) { + val defaultBackground = MaterialTheme.colorScheme.background + val background = remember { mutableStateOf(defaultBackground) } + + TranslatableRichTextViewer( + content = it, + canPreview = false, + quotesLeft = 1, + tags = EmptyTagList, + backgroundColor = background, + id = it, + accountViewModel = accountViewModel, + nav = nav, + ) + } + } + + DisplayAppRecommendations(appRecommendations, accountViewModel, nav) +} + +fun getIdentityClaimIcon(identity: IdentityClaim): Int = + when (identity) { + is TwitterIdentity -> R.drawable.x + is TelegramIdentity -> R.drawable.telegram + is MastodonIdentity -> R.drawable.mastodon + is GitHubIdentity -> R.drawable.github + else -> R.drawable.github + } + +fun getIdentityClaimDescription(identity: IdentityClaim): Int = + when (identity) { + is TwitterIdentity -> R.string.twitter + is TelegramIdentity -> R.string.telegram + is MastodonIdentity -> R.string.mastodon + is GitHubIdentity -> R.string.github + else -> R.drawable.github + } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt new file mode 100644 index 000000000..ff883a05c --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawBanner.kt @@ -0,0 +1,94 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DrawBanner( + baseUser: User, + accountViewModel: AccountViewModel, +) { + val userState by baseUser.live().metadata.observeAsState() + val banner = remember(userState) { userState?.user?.info?.banner } + + val clipboardManager = LocalClipboardManager.current + var zoomImageDialogOpen by remember { mutableStateOf(false) } + + if (!banner.isNullOrBlank()) { + AsyncImage( + model = banner, + contentDescription = stringRes(id = R.string.profile_image), + contentScale = ContentScale.FillWidth, + placeholder = painterResource(R.drawable.profile_banner), + modifier = + Modifier + .fillMaxWidth() + .height(150.dp) + .combinedClickable( + onClick = { zoomImageDialogOpen = true }, + onLongClick = { clipboardManager.setText(AnnotatedString(banner)) }, + ), + ) + + if (zoomImageDialogOpen) { + ZoomableImageDialog( + imageUrl = RichTextParser.parseImageOrVideo(banner), + onDismiss = { zoomImageDialogOpen = false }, + accountViewModel = accountViewModel, + ) + } + } else { + Image( + painter = painterResource(R.drawable.profile_banner), + contentDescription = stringRes(id = R.string.profile_banner), + contentScale = ContentScale.FillWidth, + modifier = + Modifier + .fillMaxWidth() + .height(150.dp), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditButton.kt new file mode 100644 index 000000000..ab162e830 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/EditButton.kt @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.EditNote +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.navigation.Route +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ZeroPadding + +@Composable +fun EditButton(nav: INav) { + InnerEditButton { nav.nav(Route.EditProfile.route) } +} + +@Preview +@Composable +fun InnerEditButtonPreview() { + InnerEditButton {} +} + +@Composable +fun InnerEditButton(onClick: () -> Unit) { + Button( + modifier = + Modifier + .padding(horizontal = 3.dp) + .width(50.dp), + onClick = onClick, + contentPadding = ZeroPadding, + ) { + Icon( + tint = Color.White, + imageVector = Icons.Default.EditNote, + contentDescription = stringRes(R.string.edits_the_user_s_metadata), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt new file mode 100644 index 000000000..705575cdd --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/MessageButton.kt @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size20Modifier +import com.vitorpamplona.amethyst.ui.theme.ZeroPadding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun MessageButton( + user: User, + accountViewModel: AccountViewModel, + nav: INav, +) { + val scope = rememberCoroutineScope() + + Button( + modifier = + Modifier + .padding(horizontal = 3.dp) + .width(50.dp), + onClick = { + scope.launch(Dispatchers.IO) { accountViewModel.createChatRoomFor(user) { nav.nav("Room/$it") } } + }, + contentPadding = ZeroPadding, + ) { + Icon( + painter = painterResource(R.drawable.ic_dm), + stringRes(R.string.send_a_direct_message), + modifier = Size20Modifier, + tint = Color.White, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt new file mode 100644 index 000000000..51f0ea020 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileActions.kt @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.WatchIsHiddenUser + +@Composable +fun ProfileActions( + baseUser: User, + accountViewModel: AccountViewModel, + nav: INav, +) { + val isMe by + remember(accountViewModel) { derivedStateOf { accountViewModel.userProfile() == baseUser } } + + if (isMe) { + EditButton(nav) + } + + WatchIsHiddenUser(baseUser, accountViewModel) { isHidden -> + if (isHidden) { + ShowUserButton { accountViewModel.showUser(baseUser.pubkeyHex) } + } else { + DisplayFollowUnfollowButton(baseUser, accountViewModel) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt new file mode 100644 index 000000000..39ddcb2c5 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/ProfileHeader.kt @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.commons.richtext.RichTextParser +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.NostrUserAppRecommendationsFeedViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.Size100dp +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.ZeroPadding +import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.amethyst.ui.theme.userProfileBorderModifier + +@Composable +fun ProfileHeader( + baseUser: User, + appRecommendations: NostrUserAppRecommendationsFeedViewModel, + nav: INav, + accountViewModel: AccountViewModel, +) { + var popupExpanded by remember { mutableStateOf(false) } + var zoomImageDialogOpen by remember { mutableStateOf(false) } + + Box { + DrawBanner(baseUser, accountViewModel) + + Box( + modifier = + Modifier + .statusBarsPadding() + .padding(start = 10.dp, end = 10.dp, top = 10.dp) + .size(40.dp) + .align(Alignment.TopEnd), + ) { + Button( + modifier = + Modifier + .size(30.dp) + .align(Alignment.Center), + onClick = { popupExpanded = true }, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.background, + ), + contentPadding = ZeroPadding, + ) { + Icon( + tint = MaterialTheme.colorScheme.placeholderText, + imageVector = Icons.Default.MoreVert, + contentDescription = stringRes(R.string.more_options), + ) + + UserProfileDropDownMenu( + baseUser, + popupExpanded, + { popupExpanded = false }, + accountViewModel, + ) + } + } + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp) + .padding(top = 100.dp), + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + val clipboardManager = LocalClipboardManager.current + + ClickableUserPicture( + baseUser = baseUser, + accountViewModel = accountViewModel, + size = Size100dp, + modifier = MaterialTheme.colorScheme.userProfileBorderModifier, + onClick = { + if (baseUser.profilePicture() != null) { + zoomImageDialogOpen = true + } + }, + onLongClick = { + it.info?.picture?.let { it1 -> + clipboardManager.setText( + AnnotatedString(it1), + ) + } + }, + ) + + Spacer(Modifier.weight(1f)) + + Row( + modifier = + Modifier + .height(Size35dp) + .padding(bottom = 3.dp), + ) { + MessageButton(baseUser, accountViewModel, nav) + + ProfileActions(baseUser, accountViewModel, nav) + } + } + + DrawAdditionalInfo(baseUser, appRecommendations, accountViewModel, nav) + + HorizontalDivider(modifier = Modifier.padding(top = 6.dp)) + } + } + + val profilePic = baseUser.profilePicture() + if (zoomImageDialogOpen && profilePic != null) { + ZoomableImageDialog( + RichTextParser.parseImageOrVideo(profilePic), + onDismiss = { zoomImageDialogOpen = false }, + accountViewModel = accountViewModel, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt new file mode 100644 index 000000000..79c05f7ef --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/UserProfileDropDownMenu.kt @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header + +import android.content.Intent +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.AnnotatedString +import androidx.core.content.ContextCompat +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.note.externalLinkForUser +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.DividerThickness +import com.vitorpamplona.quartz.nip56Reports.ReportEvent + +@Composable +fun UserProfileDropDownMenu( + user: User, + popupExpanded: Boolean, + onDismiss: () -> Unit, + accountViewModel: AccountViewModel, +) { + DropdownMenu( + expanded = popupExpanded, + onDismissRequest = onDismiss, + ) { + val clipboardManager = LocalClipboardManager.current + + DropdownMenuItem( + text = { Text(stringRes(R.string.copy_user_id)) }, + onClick = { + clipboardManager.setText(AnnotatedString(user.pubkeyNpub())) + onDismiss() + }, + ) + + val actContext = LocalContext.current + + DropdownMenuItem( + text = { Text(stringRes(R.string.quick_action_share)) }, + onClick = { + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra( + Intent.EXTRA_TEXT, + externalLinkForUser(user), + ) + putExtra( + Intent.EXTRA_TITLE, + stringRes(actContext, R.string.quick_action_share_browser_link), + ) + } + + val shareIntent = + Intent.createChooser(sendIntent, stringRes(actContext, R.string.quick_action_share)) + ContextCompat.startActivity(actContext, shareIntent, null) + onDismiss() + }, + ) + + if (accountViewModel.userProfile() != user) { + HorizontalDivider(thickness = DividerThickness) + if (accountViewModel.account.isHidden(user)) { + DropdownMenuItem( + text = { Text(stringRes(R.string.unblock_user)) }, + onClick = { + accountViewModel.show(user) + onDismiss() + }, + ) + } else { + DropdownMenuItem( + text = { Text(stringRes(id = R.string.block_hide_user)) }, + onClick = { + accountViewModel.hide(user) + onDismiss() + }, + ) + } + HorizontalDivider(thickness = DividerThickness) + DropdownMenuItem( + text = { Text(stringRes(id = R.string.report_spam_scam)) }, + onClick = { + accountViewModel.report(user, ReportEvent.ReportType.SPAM) + onDismiss() + }, + ) + DropdownMenuItem( + text = { Text(stringRes(R.string.report_hateful_speech)) }, + onClick = { + accountViewModel.report(user, ReportEvent.ReportType.PROFANITY) + onDismiss() + }, + ) + DropdownMenuItem( + text = { Text(stringRes(id = R.string.report_impersonation)) }, + onClick = { + accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION) + onDismiss() + }, + ) + DropdownMenuItem( + text = { Text(stringRes(R.string.report_nudity_porn)) }, + onClick = { + accountViewModel.report(user, ReportEvent.ReportType.NUDITY) + onDismiss() + }, + ) + DropdownMenuItem( + text = { Text(stringRes(id = R.string.report_illegal_behaviour)) }, + onClick = { + accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL) + onDismiss() + }, + ) + DropdownMenuItem( + text = { Text(stringRes(id = R.string.report_malware)) }, + onClick = { + accountViewModel.report(user, ReportEvent.ReportType.MALWARE) + onDismiss() + }, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt new file mode 100644 index 000000000..14aa1f004 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/DisplayAppRecommendations.kt @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps + +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled +import com.vitorpamplona.amethyst.ui.feeds.FeedState +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes + +@Composable +fun DisplayAppRecommendations( + appRecommendations: NostrUserAppRecommendationsFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + val feedState by appRecommendations.feedState.feedContent.collectAsStateWithLifecycle() + + LaunchedEffect(key1 = Unit) { appRecommendations.invalidateData() } + + CrossfadeIfEnabled( + targetState = feedState, + animationSpec = tween(durationMillis = 100), + accountViewModel = accountViewModel, + ) { state -> + when (state) { + is FeedState.Loaded -> { + Column { + Text(stringRes(id = R.string.recommended_apps)) + + Recommends(state, nav) + } + } + else -> {} + } + } +} + +@Composable +@OptIn(ExperimentalLayoutApi::class) +fun Recommends( + loaded: FeedState.Loaded, + nav: INav, +) { + val items by loaded.feed.collectAsStateWithLifecycle() + FlowRow( + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(vertical = 5.dp), + ) { + items.list.forEach { app -> WatchApp(app, nav) } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/NostrUserAppRecommendationsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/NostrUserAppRecommendationsFeedViewModel.kt new file mode 100644 index 000000000..fdc0bf6c4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/NostrUserAppRecommendationsFeedViewModel.kt @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileAppRecommendationsFeedFilter +import com.vitorpamplona.amethyst.ui.screen.FeedViewModel + +class NostrUserAppRecommendationsFeedViewModel( + val user: User, +) : FeedViewModel(UserProfileAppRecommendationsFeedFilter(user)) { + class Factory( + val user: User, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserAppRecommendationsFeedViewModel = + NostrUserAppRecommendationsFeedViewModel(user) + as NostrUserAppRecommendationsFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt new file mode 100644 index 000000000..3d935555e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/apps/WatchApp.kt @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import coil3.compose.AsyncImage +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun WatchApp( + baseApp: Note, + nav: INav, +) { + val appState by baseApp.live().metadata.observeAsState() + + var appLogo by remember(baseApp) { mutableStateOf(null) } + var appName by remember(baseApp) { mutableStateOf(null) } + + LaunchedEffect(key1 = appState) { + withContext(Dispatchers.Default) { + (appState?.note?.event as? AppDefinitionEvent)?.appMetaData()?.let { metaData -> + metaData.picture?.ifBlank { null }?.let { newLogo -> + if (newLogo != appLogo) appLogo = newLogo + } + metaData.name?.ifBlank { null }?.let { newName -> + if (newName != appName) appName = newName + } + } + } + } + + appLogo?.let { + Box( + remember { + Modifier + .size(Size35dp) + .clickable { nav.nav("Note/${baseApp.idHex}") } + }, + ) { + AsyncImage( + model = appLogo, + contentDescription = appName, + modifier = + remember { + Modifier + .size(Size35dp) + .clip(shape = CircleShape) + }, + ) + } + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt new file mode 100644 index 000000000..1c116cf6e --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/badges/DisplayBadges.kt @@ -0,0 +1,242 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.badges + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CutCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.distinctUntilChanged +import androidx.lifecycle.map +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.AddressableNote +import com.vitorpamplona.amethyst.model.FeatureSetType +import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage +import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag +import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent +import com.vitorpamplona.quartz.nip58Badges.BadgeProfilesEvent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun DisplayBadges( + baseUser: User, + accountViewModel: AccountViewModel, + nav: INav, +) { + LoadAddressableNote( + BadgeProfilesEvent.createAddress(baseUser.pubkeyHex), + accountViewModel, + ) { note -> + if (note != null) { + WatchAndRenderBadgeList( + note = note, + loadProfilePicture = accountViewModel.settings.showProfilePictures.value, + loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE, + nav = nav, + ) + } + } +} + +@Composable +private fun WatchAndRenderBadgeList( + note: AddressableNote, + loadProfilePicture: Boolean, + loadRobohash: Boolean, + nav: INav, +) { + val badgeList by + note + .live() + .metadata + .map { (it.note.event as? BadgeProfilesEvent)?.badgeAwardEvents()?.toImmutableList() } + .distinctUntilChanged() + .observeAsState() + + badgeList?.let { list -> RenderBadgeList(list, loadProfilePicture, loadRobohash, nav) } +} + +@Composable +@OptIn(ExperimentalLayoutApi::class) +private fun RenderBadgeList( + list: ImmutableList, + loadProfilePicture: Boolean, + loadRobohash: Boolean, + nav: INav, +) { + FlowRow( + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(vertical = 5.dp), + ) { + list.forEach { badgeAwardEvent -> LoadAndRenderBadge(badgeAwardEvent, loadProfilePicture, loadRobohash, nav) } + } +} + +@Composable +private fun LoadAndRenderBadge( + badgeAwardEvent: ETag, + loadProfilePicture: Boolean, + loadRobohash: Boolean, + nav: INav, +) { + var baseNote by remember(badgeAwardEvent) { mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEvent)) } + + LaunchedEffect(key1 = badgeAwardEvent) { + if (baseNote == null) { + withContext(Dispatchers.IO) { + baseNote = LocalCache.checkGetOrCreateNote(badgeAwardEvent) + } + } + } + + baseNote?.let { ObserveAndRenderBadge(it, loadProfilePicture, loadRobohash, nav) } +} + +@Composable +private fun ObserveAndRenderBadge( + it: Note, + loadProfilePicture: Boolean, + loadRobohash: Boolean, + nav: INav, +) { + val badgeAwardState by it.live().metadata.observeAsState() + val baseBadgeDefinition by + remember(badgeAwardState) { derivedStateOf { badgeAwardState?.note?.replyTo?.firstOrNull() } } + + baseBadgeDefinition?.let { BadgeThumb(it, loadProfilePicture, loadRobohash, nav, Size35dp) } +} + +@Composable +fun BadgeThumb( + note: Note, + loadProfilePicture: Boolean, + loadRobohash: Boolean, + nav: INav, + size: Dp, + pictureModifier: Modifier = Modifier, +) { + BadgeThumb(note, loadProfilePicture, loadRobohash, size, pictureModifier) { nav.nav("Note/${note.idHex}") } +} + +@Composable +fun BadgeThumb( + baseNote: Note, + loadProfilePicture: Boolean, + loadRobohash: Boolean, + size: Dp, + pictureModifier: Modifier = Modifier, + onClick: ((String) -> Unit)? = null, +) { + Box( + remember { + Modifier + .width(size) + .height(size) + }, + ) { + WatchAndRenderBadgeImage(baseNote, loadProfilePicture, loadRobohash, size, pictureModifier, onClick) + } +} + +@Composable +private fun WatchAndRenderBadgeImage( + baseNote: Note, + loadProfilePicture: Boolean, + loadRobohash: Boolean, + size: Dp, + pictureModifier: Modifier, + onClick: ((String) -> Unit)?, +) { + val noteState by baseNote.live().metadata.observeAsState() + val eventId = remember(noteState) { noteState?.note?.idHex } ?: return + val image by + remember(noteState) { + derivedStateOf { + val event = noteState?.note?.event as? BadgeDefinitionEvent + event?.thumb()?.ifBlank { null } ?: event?.image()?.ifBlank { null } + } + } + + if (image == null) { + RobohashAsyncImage( + robot = "authornotfound", + contentDescription = stringRes(R.string.unknown_author), + modifier = + remember { + pictureModifier + .width(size) + .height(size) + }, + loadRobohash = loadRobohash, + ) + } else { + RobohashFallbackAsyncImage( + robot = eventId, + model = image!!, + contentDescription = stringRes(id = R.string.profile_image), + modifier = + remember { + pictureModifier + .width(size) + .height(size) + .clip(shape = CutCornerShape(20)) + .run { + if (onClick != null) { + this.clickable(onClick = { onClick(eventId) }) + } else { + this + } + } + }, + loadProfilePicture = loadProfilePicture, + loadRobohash = loadRobohash, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/NostrUserProfileMutualFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/NostrUserProfileMutualFeedViewModel.kt new file mode 100644 index 000000000..f234e96e7 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/NostrUserProfileMutualFeedViewModel.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileMutualFeedFilter +import com.vitorpamplona.amethyst.ui.screen.FeedViewModel + +class NostrUserProfileMutualFeedViewModel( + val user: User, + val account: Account, +) : FeedViewModel(UserProfileMutualFeedFilter(user, account)) { + class Factory( + val user: User, + val account: Account, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileMutualFeedViewModel = + NostrUserProfileMutualFeedViewModel(user, account) + as NostrUserProfileMutualFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/TabMutualConversations.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/TabMutualConversations.kt new file mode 100644 index 000000000..c06a0d66d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/mutual/TabMutualConversations.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabMutualConversations( + feedViewModel: NostrUserProfileMutualFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + Column(Modifier.fillMaxHeight()) { + RefresheableFeedView( + feedViewModel, + null, + enablePullRefresh = false, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/NostrUserProfileNewThreadsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/NostrUserProfileNewThreadsFeedViewModel.kt new file mode 100644 index 000000000..1032d0bef --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/NostrUserProfileNewThreadsFeedViewModel.kt @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter +import com.vitorpamplona.amethyst.ui.screen.FeedViewModel + +class NostrUserProfileNewThreadsFeedViewModel( + val user: User, + val account: Account, +) : FeedViewModel(UserProfileNewThreadFeedFilter(user, account)) { + class Factory( + val user: User, + val account: Account, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileNewThreadsFeedViewModel = + NostrUserProfileNewThreadsFeedViewModel(user, account) + as NostrUserProfileNewThreadsFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt new file mode 100644 index 000000000..afb8cbb4d --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/newthreads/TabNotesNewThreads.kt @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabNotesNewThreads( + feedViewModel: NostrUserProfileNewThreadsFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + Column(Modifier.fillMaxHeight()) { + RefresheableFeedView( + feedViewModel, + null, + enablePullRefresh = false, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/RelayFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt similarity index 97% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/RelayFeedView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt index eedeb0a03..d9eee03e1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/RelayFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedView.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/RelayFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/RelayFeedViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt index 0d6257a42..a40162b8d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/RelayFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelayFeedViewModel.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays import android.util.Log import androidx.compose.runtime.MutableState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt new file mode 100644 index 000000000..5b000bc80 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/RelaysTabHeader.kt @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays + +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.stringRes + +@Composable +fun RelaysTabHeader(baseUser: User) { + val userState by baseUser.live().relays.observeAsState() + val userRelaysBeingUsed = remember(userState) { userState?.user?.relaysBeingUsed?.size ?: "--" } + + val userStateRelayInfo by baseUser.live().relayInfo.observeAsState() + val userRelays = + remember(userStateRelayInfo) { + userStateRelayInfo + ?.user + ?.latestContactList + ?.relays() + ?.size ?: "--" + } + + Text(text = "$userRelaysBeingUsed / $userRelays ${stringRes(R.string.relays)}") +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt new file mode 100644 index 000000000..6b2a036f6 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/relays/TabRelays.kt @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.Modifier +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabRelays( + user: User, + accountViewModel: AccountViewModel, + nav: INav, +) { + val feedViewModel: RelayFeedViewModel = viewModel() + + val lifeCycleOwner = LocalLifecycleOwner.current + + DisposableEffect(user) { + feedViewModel.subscribeTo(user) + onDispose { feedViewModel.unsubscribeTo(user) } + } + + DisposableEffect(lifeCycleOwner) { + val observer = + LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + println("Profile Relay Start") + feedViewModel.subscribeTo(user) + } + if (event == Lifecycle.Event.ON_PAUSE) { + println("Profile Relay Stop") + feedViewModel.unsubscribeTo(user) + } + } + + lifeCycleOwner.lifecycle.addObserver(observer) + onDispose { + lifeCycleOwner.lifecycle.removeObserver(observer) + println("Profile Relay Dispose") + feedViewModel.unsubscribeTo(user) + } + } + + Column(Modifier.fillMaxHeight()) { + RelayFeedView(feedViewModel, accountViewModel, enablePullRefresh = false, nav = nav) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/NostrUserProfileReportFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/NostrUserProfileReportFeedViewModel.kt new file mode 100644 index 000000000..4aee303be --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/NostrUserProfileReportFeedViewModel.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter +import com.vitorpamplona.amethyst.ui.screen.FeedViewModel + +class NostrUserProfileReportFeedViewModel( + val user: User, +) : FeedViewModel(UserProfileReportsFeedFilter(user)) { + class Factory( + val user: User, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileReportFeedViewModel = NostrUserProfileReportFeedViewModel(user) as NostrUserProfileReportFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt new file mode 100644 index 000000000..8f9933148 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/ReportsTabHeader.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports + +import androidx.compose.material3.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.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter +import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun ReportsTabHeader(baseUser: User) { + val userState by baseUser.live().reports.observeAsState() + var userReports by remember { mutableIntStateOf(0) } + + LaunchedEffect(key1 = userState) { + launch(Dispatchers.IO) { + val newSize = UserProfileReportsFeedFilter(baseUser).feed().size + + if (newSize != userReports) { + userReports = newSize + } + } + } + + Text(text = "$userReports ${stringRes(R.string.reports)}") +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt new file mode 100644 index 000000000..784d86f45 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/TabReports.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.RefresheableFeedView +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabReports( + baseUser: User, + feedViewModel: NostrUserProfileReportFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + WatchReportsAndUpdateFeed(baseUser, feedViewModel) + + Column(Modifier.fillMaxHeight()) { + RefresheableFeedView( + feedViewModel, + null, + enablePullRefresh = false, + accountViewModel = accountViewModel, + nav = nav, + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt new file mode 100644 index 000000000..78cd74172 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/reports/WatchReportsAndUpdateFeed.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import com.vitorpamplona.amethyst.model.User + +@Composable +fun WatchReportsAndUpdateFeed( + baseUser: User, + feedViewModel: NostrUserProfileReportFeedViewModel, +) { + val userState by baseUser.live().reports.observeAsState() + LaunchedEffect(userState) { feedViewModel.invalidateData() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedState.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt similarity index 96% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedState.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt index e9b59483c..b2a3f2cf3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedState.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedState.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt similarity index 98% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedView.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt index c3e16f657..440e42123 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedView.kt @@ -18,7 +18,7 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.padding diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt similarity index 86% rename from amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedViewModel.kt rename to amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt index 9f6067c2b..802e44863 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/LnZapFeedViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/LnZapFeedViewModel.kt @@ -18,19 +18,16 @@ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps import android.util.Log import androidx.compose.runtime.Stable import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.vitorpamplona.amethyst.model.LocalCache -import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.ui.dal.FeedFilter -import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.equalImmutableLists import com.vitorpamplona.ammolite.relays.BundledUpdate import kotlinx.collections.immutable.ImmutableList @@ -42,16 +39,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class NostrUserProfileZapsFeedViewModel( - user: User, -) : LnZapFeedViewModel(UserProfileZapsFeedFilter(user)) { - class Factory( - val user: User, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): NostrUserProfileZapsFeedViewModel = NostrUserProfileZapsFeedViewModel(user) as NostrUserProfileZapsFeedViewModel - } -} - @Stable open class LnZapFeedViewModel( val dataSource: FeedFilter, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/NostrUserProfileZapsFeedViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/NostrUserProfileZapsFeedViewModel.kt new file mode 100644 index 000000000..f1d8a66ac --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/NostrUserProfileZapsFeedViewModel.kt @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter + +class NostrUserProfileZapsFeedViewModel( + user: User, +) : LnZapFeedViewModel(UserProfileZapsFeedFilter(user)) { + class Factory( + val user: User, + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): NostrUserProfileZapsFeedViewModel = NostrUserProfileZapsFeedViewModel(user) as NostrUserProfileZapsFeedViewModel + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ShowUserButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ShowUserButton.kt new file mode 100644 index 000000000..7def995e8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ShowUserButton.kt @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.stringRes +import com.vitorpamplona.amethyst.ui.theme.ButtonBorder +import com.vitorpamplona.amethyst.ui.theme.ButtonPadding + +@Composable +fun ShowUserButton(onClick: () -> Unit) { + Button( + modifier = Modifier.padding(start = 3.dp), + onClick = onClick, + shape = ButtonBorder, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + contentPadding = ButtonPadding, + ) { + Text(text = stringRes(R.string.unblock), color = Color.White) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt new file mode 100644 index 000000000..0355ca81a --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/TabReceivedZaps.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.navigation.INav +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun TabReceivedZaps( + baseUser: User, + zapFeedViewModel: NostrUserProfileZapsFeedViewModel, + accountViewModel: AccountViewModel, + nav: INav, +) { + WatchZapsAndUpdateFeed(baseUser, zapFeedViewModel) + + Column(Modifier.fillMaxHeight()) { + LnZapFeedView(zapFeedViewModel, accountViewModel, nav) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchIsHiddenUser.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchIsHiddenUser.kt new file mode 100644 index 000000000..c66cf9767 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchIsHiddenUser.kt @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.lifecycle.map +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel + +@Composable +fun WatchIsHiddenUser( + baseUser: User, + accountViewModel: AccountViewModel, + content: @Composable (Boolean) -> Unit, +) { + val isHidden by + accountViewModel.account.liveHiddenUsers + .map { + it.hiddenUsers.contains(baseUser.pubkeyHex) || it.spammers.contains(baseUser.pubkeyHex) + }.observeAsState(accountViewModel.account.isHidden(baseUser)) + + content(isHidden) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt new file mode 100644 index 000000000..cd2fec5d0 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/WatchZapsAndUpdateFeed.kt @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import com.vitorpamplona.amethyst.model.User + +@Composable +fun WatchZapsAndUpdateFeed( + baseUser: User, + feedViewModel: NostrUserProfileZapsFeedViewModel, +) { + val userState by baseUser.live().zaps.observeAsState() + + LaunchedEffect(userState) { feedViewModel.invalidateData() } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt new file mode 100644 index 000000000..1a8eb58ef --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/zaps/ZapTabHeader.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps + +import androidx.compose.material3.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.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.User +import com.vitorpamplona.amethyst.ui.note.showAmountInteger +import com.vitorpamplona.amethyst.ui.stringRes +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.math.BigDecimal + +@Composable +fun ZapTabHeader(baseUser: User) { + val userState by baseUser.live().zaps.observeAsState() + var zapAmount by remember { mutableStateOf(null) } + + LaunchedEffect(key1 = userState) { + launch(Dispatchers.Default) { + val tempAmount = baseUser.zappedAmount() + if (zapAmount != tempAmount) { + zapAmount = tempAmount + } + } + } + + Text(text = "${showAmountInteger(zapAmount)} ${stringRes(id = R.string.zaps)}") +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 6f14f9662..45d713956 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -146,6 +146,7 @@ Conversations Notes Replies + Yours Gallery "Follows" "Reports" diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt index b174d3ff8..e11d0bd26 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/tags/addressables/ATag.kt @@ -57,6 +57,9 @@ data class ATag( const val TAG_NAME = "a" const val TAG_SIZE = 2 + @JvmStatic + fun isTagged(tag: Array) = tag.size >= 2 && tag[0] == TAG_NAME && tag[1].isNotEmpty() + @JvmStatic fun isSameAddress( tag1: Array, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt index 75070bb96..b4b57b875 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip51Lists/BookmarkListEvent.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag +import com.vitorpamplona.quartz.nip01Core.tags.events.ETag import com.vitorpamplona.quartz.nip31Alts.AltTag import com.vitorpamplona.quartz.utils.TimeUtils @@ -36,6 +37,8 @@ class BookmarkListEvent( content: String, sig: HexKey, ) : GeneralListEvent(id, pubKey, createdAt, KIND, tags, content, sig) { + fun countBookmarks() = tags.count(ETag::isTagged) + tags.count(ATag::isTagged) + companion object { const val KIND = 30001 const val ALT = "List of bookmarks"