Adds the logged in user' posts to a user in the user's profile.

Refactors the user profile screen in many packages.
This commit is contained in:
Vitor Pamplona
2025-02-25 18:09:04 -05:00
parent e6cd3a99c5
commit b40b37b6ac
59 changed files with 3299 additions and 1715 deletions
@@ -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<Note>() {
override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + user.pubkeyHex
override fun feed(): List<Note> {
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<Note>): Set<Note> = innerApplyFilter(collection)
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> = 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<Note>): List<Note> = collection.sortedWith(DefaultFeedOrder)
override fun limit() = 200
}
@@ -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(
@@ -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
@@ -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 <NostrUserProfileNewThreadsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileNewThreadsFeedViewModel>): 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 <NostrUserProfileConversationsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileConversationsFeedViewModel>): 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 <NostrUserProfileReportFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileReportFeedViewModel>): 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 <NostrUserProfileGalleryFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileGalleryFeedViewModel>): 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 <NostrUserProfileBookmarksFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileBookmarksFeedViewModel>): 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 <NostrUserAppRecommendationsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserAppRecommendationsFeedViewModel>): NostrUserAppRecommendationsFeedViewModel =
NostrUserAppRecommendationsFeedViewModel(user)
as NostrUserAppRecommendationsFeedViewModel
}
}
abstract class LevelFeedViewModel(
localFilter: FeedFilter<Note>,
) : FeedViewModel(localFilter) {
@@ -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 <NostrUserProfileFollowsUserFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileFollowsUserFeedViewModel>): 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 <NostrUserProfileFollowersUserFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileFollowersUserFeedViewModel>): NostrUserProfileFollowersUserFeedViewModel =
NostrUserProfileFollowersUserFeedViewModel(user, account)
as NostrUserProfileFollowersUserFeedViewModel
}
}
class NostrHiddenAccountsFeedViewModel(
val account: Account,
) : UserFeedViewModel(HiddenAccountsFeedFilter(account)) {
@@ -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)
}
}
@@ -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)}")
}
@@ -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 <NostrUserProfileBookmarksFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileBookmarksFeedViewModel>): NostrUserProfileBookmarksFeedViewModel =
NostrUserProfileBookmarksFeedViewModel(user, account)
as NostrUserProfileBookmarksFeedViewModel
}
}
@@ -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,
)
}
}
}
@@ -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 <NostrUserProfileConversationsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileConversationsFeedViewModel>): NostrUserProfileConversationsFeedViewModel =
NostrUserProfileConversationsFeedViewModel(user, account)
as NostrUserProfileConversationsFeedViewModel
}
}
@@ -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,
)
}
}
@@ -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)
}
@@ -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 <NostrUserProfileFollowersUserFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileFollowersUserFeedViewModel>): NostrUserProfileFollowersUserFeedViewModel =
NostrUserProfileFollowersUserFeedViewModel(user, account)
as NostrUserProfileFollowersUserFeedViewModel
}
}
@@ -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() }
}
@@ -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)
}
@@ -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 <NostrUserProfileFollowsUserFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileFollowsUserFeedViewModel>): NostrUserProfileFollowsUserFeedViewModel =
NostrUserProfileFollowsUserFeedViewModel(user, account)
as NostrUserProfileFollowsUserFeedViewModel
}
}
@@ -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() }
}
@@ -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 <NostrUserProfileGalleryFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileGalleryFeedViewModel>): NostrUserProfileGalleryFeedViewModel =
NostrUserProfileGalleryFeedViewModel(user, account)
as NostrUserProfileGalleryFeedViewModel
}
}
@@ -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,
)
}
}
}
@@ -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)}")
}
@@ -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,
)
}
}
}
}
}
@@ -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)
}
}
}
}
}
@@ -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<String?>(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<String?>(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) },
)
}
}
}
}
@@ -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
}
@@ -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),
)
}
}
@@ -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),
)
}
}
@@ -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,
)
}
}
@@ -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)
}
}
}
@@ -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,
)
}
}
@@ -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()
},
)
}
}
}
@@ -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) }
}
}
@@ -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 <NostrUserAppRecommendationsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserAppRecommendationsFeedViewModel>): NostrUserAppRecommendationsFeedViewModel =
NostrUserAppRecommendationsFeedViewModel(user)
as NostrUserAppRecommendationsFeedViewModel
}
}
@@ -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<String?>(null) }
var appName by remember(baseApp) { mutableStateOf<String?>(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)
},
)
}
}
}
@@ -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<ETag>,
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,
)
}
}
@@ -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 <NostrUserProfileMutualFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileMutualFeedViewModel>): NostrUserProfileMutualFeedViewModel =
NostrUserProfileMutualFeedViewModel(user, account)
as NostrUserProfileMutualFeedViewModel
}
}
@@ -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,
)
}
}
@@ -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 <NostrUserProfileNewThreadsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileNewThreadsFeedViewModel>): NostrUserProfileNewThreadsFeedViewModel =
NostrUserProfileNewThreadsFeedViewModel(user, account)
as NostrUserProfileNewThreadsFeedViewModel
}
}
@@ -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,
)
}
}
@@ -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
@@ -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
@@ -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)}")
}
@@ -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)
}
}
@@ -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 <NostrUserProfileReportFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileReportFeedViewModel>): NostrUserProfileReportFeedViewModel = NostrUserProfileReportFeedViewModel(user) as NostrUserProfileReportFeedViewModel
}
}
@@ -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)}")
}
@@ -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,
)
}
}
@@ -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() }
}
@@ -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
@@ -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
@@ -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 <NostrUserProfileZapsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileZapsFeedViewModel>): NostrUserProfileZapsFeedViewModel = NostrUserProfileZapsFeedViewModel(user) as NostrUserProfileZapsFeedViewModel
}
}
@Stable
open class LnZapFeedViewModel(
val dataSource: FeedFilter<ZapReqResponse>,
@@ -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 <NostrUserProfileZapsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileZapsFeedViewModel>): NostrUserProfileZapsFeedViewModel = NostrUserProfileZapsFeedViewModel(user) as NostrUserProfileZapsFeedViewModel
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
@@ -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() }
}
@@ -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<BigDecimal?>(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)}")
}
+1
View File
@@ -146,6 +146,7 @@
<string name="conversations">Conversations</string>
<string name="notes">Notes</string>
<string name="replies">Replies</string>
<string name="mutual">Yours</string>
<string name="gallery">Gallery</string>
<string name="follows">"Follows"</string>
<string name="reports">"Reports"</string>
@@ -57,6 +57,9 @@ data class ATag(
const val TAG_NAME = "a"
const val TAG_SIZE = 2
@JvmStatic
fun isTagged(tag: Array<String>) = tag.size >= 2 && tag[0] == TAG_NAME && tag[1].isNotEmpty()
@JvmStatic
fun isSameAddress(
tag1: Array<String>,
@@ -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"