mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-11-11 03:56:43 +01:00
Completely change the UI for list management when adding/removing a profile from lists.
This commit is contained in:
@@ -85,6 +85,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationS
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.publicMessages.NewPublicMessageScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.FollowSetsManagementDialog
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.redirect.LoadRedirectScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationScreen
|
||||||
@@ -130,6 +131,9 @@ fun AppNavigation(
|
|||||||
composableArgs<Route.FollowSetRoute> {
|
composableArgs<Route.FollowSetRoute> {
|
||||||
FollowSetScreen(it.setIdentifier, accountViewModel, listsViewModel, nav)
|
FollowSetScreen(it.setIdentifier, accountViewModel, listsViewModel, nav)
|
||||||
}
|
}
|
||||||
|
composableArgs<Route.FollowSetManagement> {
|
||||||
|
FollowSetsManagementDialog(it.userHexKey, accountViewModel.account, listsViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
composable<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
composable<Route.EditProfile> { NewUserMetadataScreen(nav, accountViewModel) }
|
||||||
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
composable<Route.Search> { SearchScreen(accountViewModel, nav) }
|
||||||
@@ -145,7 +149,7 @@ fun AppNavigation(
|
|||||||
composableFromEndArgs<Route.EditMediaServers> { AllMediaServersScreen(accountViewModel, nav) }
|
composableFromEndArgs<Route.EditMediaServers> { AllMediaServersScreen(accountViewModel, nav) }
|
||||||
|
|
||||||
composableFromEndArgs<Route.ContentDiscovery> { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) }
|
composableFromEndArgs<Route.ContentDiscovery> { DvmContentDiscoveryScreen(it.id, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.Profile> { ProfileScreen(it.id, accountViewModel, listsViewModel, nav) }
|
composableFromEndArgs<Route.Profile> { ProfileScreen(it.id, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.Note> { ThreadScreen(it.id, accountViewModel, nav) }
|
composableFromEndArgs<Route.Note> { ThreadScreen(it.id, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.Hashtag> { HashtagScreen(it, accountViewModel, nav) }
|
composableFromEndArgs<Route.Hashtag> { HashtagScreen(it, accountViewModel, nav) }
|
||||||
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
composableFromEndArgs<Route.Geohash> { GeoHashScreen(it, accountViewModel, nav) }
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ sealed class Route {
|
|||||||
val setIdentifier: String,
|
val setIdentifier: String,
|
||||||
) : Route()
|
) : Route()
|
||||||
|
|
||||||
|
@Serializable data class FollowSetManagement(
|
||||||
|
val userHexKey: String,
|
||||||
|
) : Route()
|
||||||
|
|
||||||
@Serializable object EditProfile : Route()
|
@Serializable object EditProfile : Route()
|
||||||
|
|
||||||
@Serializable object EditRelays : Route()
|
@Serializable object EditRelays : Route()
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observe
|
|||||||
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
import com.vitorpamplona.amethyst.ui.feeds.WatchLifecycleAndUpdateModel
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.BookmarkTabHeader
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.BookmarkTabHeader
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.TabBookmarks
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.TabBookmarks
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.bookmarks.dal.UserProfileBookmarksFeedViewModel
|
||||||
@@ -104,7 +103,6 @@ import kotlinx.coroutines.launch
|
|||||||
fun ProfileScreen(
|
fun ProfileScreen(
|
||||||
userId: String?,
|
userId: String?,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nostrListsViewModel: NostrUserListFeedViewModel,
|
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
if (userId == null) return
|
if (userId == null) return
|
||||||
@@ -124,7 +122,6 @@ fun ProfileScreen(
|
|||||||
PrepareViewModels(
|
PrepareViewModels(
|
||||||
baseUser = it,
|
baseUser = it,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nostrListsViewModel = nostrListsViewModel,
|
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -134,7 +131,6 @@ fun ProfileScreen(
|
|||||||
fun PrepareViewModels(
|
fun PrepareViewModels(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nostrListsViewModel: NostrUserListFeedViewModel,
|
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val followsFeedViewModel: UserProfileFollowsUserFeedViewModel =
|
val followsFeedViewModel: UserProfileFollowsUserFeedViewModel =
|
||||||
@@ -243,7 +239,6 @@ fun PrepareViewModels(
|
|||||||
bookmarksFeedViewModel,
|
bookmarksFeedViewModel,
|
||||||
galleryFeedViewModel,
|
galleryFeedViewModel,
|
||||||
reportsFeedViewModel,
|
reportsFeedViewModel,
|
||||||
nostrListsViewModel,
|
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
@@ -262,7 +257,6 @@ fun ProfileScreen(
|
|||||||
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
||||||
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
||||||
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
||||||
followSetsViewModel: NostrUserListFeedViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
@@ -275,7 +269,6 @@ fun ProfileScreen(
|
|||||||
WatchLifecycleAndUpdateModel(bookmarksFeedViewModel)
|
WatchLifecycleAndUpdateModel(bookmarksFeedViewModel)
|
||||||
WatchLifecycleAndUpdateModel(galleryFeedViewModel)
|
WatchLifecycleAndUpdateModel(galleryFeedViewModel)
|
||||||
WatchLifecycleAndUpdateModel(reportsFeedViewModel)
|
WatchLifecycleAndUpdateModel(reportsFeedViewModel)
|
||||||
WatchLifecycleAndUpdateModel(followSetsViewModel)
|
|
||||||
|
|
||||||
UserProfileFilterAssemblerSubscription(baseUser, accountViewModel.dataSources().profile)
|
UserProfileFilterAssemblerSubscription(baseUser, accountViewModel.dataSources().profile)
|
||||||
|
|
||||||
@@ -294,7 +287,6 @@ fun ProfileScreen(
|
|||||||
bookmarksFeedViewModel,
|
bookmarksFeedViewModel,
|
||||||
galleryFeedViewModel,
|
galleryFeedViewModel,
|
||||||
reportsFeedViewModel,
|
reportsFeedViewModel,
|
||||||
followSetsViewModel,
|
|
||||||
accountViewModel,
|
accountViewModel,
|
||||||
nav,
|
nav,
|
||||||
)
|
)
|
||||||
@@ -388,14 +380,13 @@ private fun RenderScreen(
|
|||||||
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
bookmarksFeedViewModel: UserProfileBookmarksFeedViewModel,
|
||||||
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
galleryFeedViewModel: UserProfileGalleryFeedViewModel,
|
||||||
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
reportsFeedViewModel: UserProfileReportFeedViewModel,
|
||||||
followSetsViewModel: NostrUserListFeedViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val pagerState = rememberPagerState { 11 }
|
val pagerState = rememberPagerState { 11 }
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
ProfileHeader(baseUser, appRecommendations, followSetsViewModel, nav, accountViewModel)
|
ProfileHeader(baseUser, appRecommendations, nav, accountViewModel)
|
||||||
ScrollableTabRow(
|
ScrollableTabRow(
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
|||||||
@@ -1,265 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 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.border
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
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.padding
|
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.PlaylistAddCheck
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
|
||||||
import androidx.compose.material.icons.filled.ArrowDropUp
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material3.AssistChip
|
|
||||||
import androidx.compose.material3.AssistChipDefaults
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.PopupProperties
|
|
||||||
import com.vitorpamplona.amethyst.R
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSet
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FollowSetsActionMenu(
|
|
||||||
isMenuOpen: Boolean,
|
|
||||||
setMenuOpenState: () -> Unit,
|
|
||||||
userHex: String,
|
|
||||||
followLists: List<FollowSet>,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
addUser: (index: Int, userPubkey: String, list: FollowSet) -> Unit,
|
|
||||||
removeUser: (index: Int, userPubkey: String, list: FollowSet) -> Unit,
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
TextButton(
|
|
||||||
onClick = setMenuOpenState,
|
|
||||||
shape = ButtonBorder.copy(topStart = CornerSize(0f), bottomStart = CornerSize(0f)),
|
|
||||||
colors =
|
|
||||||
ButtonDefaults
|
|
||||||
.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
|
|
||||||
contentPadding = ZeroPadding,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (isMenuOpen) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown,
|
|
||||||
contentDescription = "",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = isMenuOpen,
|
|
||||||
onDismissRequest = setMenuOpenState,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
properties = PopupProperties(usePlatformDefaultWidth = true),
|
|
||||||
) {
|
|
||||||
DropDownMenuHeader(headerText = "Add to lists")
|
|
||||||
followLists.forEachIndexed { index, list ->
|
|
||||||
Spacer(StdVertSpacer)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
FollowSetItem(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
listHeader = list.title,
|
|
||||||
listVisibility = list.visibility,
|
|
||||||
isUserInList = list.profileList.contains(userHex),
|
|
||||||
onRemoveUser = {
|
|
||||||
removeUser(index, userHex, list)
|
|
||||||
},
|
|
||||||
onAddUser = {
|
|
||||||
addUser(index, userHex, list)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = {},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun DropDownMenuHeader(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
headerText: String,
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = {
|
|
||||||
Text(text = headerText, fontWeight = FontWeight.SemiBold)
|
|
||||||
},
|
|
||||||
onClick = {},
|
|
||||||
enabled = false,
|
|
||||||
)
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun FollowSetItem(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
listHeader: String,
|
|
||||||
listVisibility: ListVisibility,
|
|
||||||
isUserInList: Boolean,
|
|
||||||
onAddUser: () -> Unit,
|
|
||||||
onRemoveUser: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
modifier
|
|
||||||
// .clickable(onClick = onAddUser)
|
|
||||||
.border(
|
|
||||||
width = Dp.Hairline,
|
|
||||||
color = Color.Gray,
|
|
||||||
shape = RoundedCornerShape(percent = 20),
|
|
||||||
).padding(all = 10.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier.weight(1f),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Text(listHeader, fontWeight = FontWeight.Bold)
|
|
||||||
Spacer(modifier = StdVertSpacer)
|
|
||||||
Row {
|
|
||||||
FilterChip(
|
|
||||||
selected = isUserInList,
|
|
||||||
enabled = isUserInList,
|
|
||||||
onClick = {},
|
|
||||||
label = {
|
|
||||||
Text(text = if (isUserInList) "In List" else "Not in List")
|
|
||||||
},
|
|
||||||
leadingIcon =
|
|
||||||
if (isUserInList) {
|
|
||||||
{
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.PlaylistAddCheck,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
shape = ButtonBorder,
|
|
||||||
)
|
|
||||||
Spacer(modifier = StdHorzSpacer)
|
|
||||||
AssistChip(
|
|
||||||
onClick = {
|
|
||||||
if (isUserInList) onRemoveUser() else onAddUser()
|
|
||||||
},
|
|
||||||
label = {
|
|
||||||
Text(text = if (isUserInList) "Remove" else "Add")
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
if (isUserInList) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Delete,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Add,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape = ButtonBorder,
|
|
||||||
colors =
|
|
||||||
AssistChipDefaults.assistChipColors(
|
|
||||||
containerColor =
|
|
||||||
if (isUserInList) {
|
|
||||||
MaterialTheme.colorScheme.errorContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
},
|
|
||||||
),
|
|
||||||
border =
|
|
||||||
AssistChipDefaults
|
|
||||||
.assistChipBorder(
|
|
||||||
enabled = true,
|
|
||||||
borderColor =
|
|
||||||
if (!isUserInList) {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.errorContainer
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listVisibility.let {
|
|
||||||
val text by derivedStateOf {
|
|
||||||
when (it) {
|
|
||||||
ListVisibility.Public -> "Public"
|
|
||||||
ListVisibility.Private -> "Private"
|
|
||||||
ListVisibility.Mixed -> "Mixed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter =
|
|
||||||
painterResource(
|
|
||||||
when (it) {
|
|
||||||
ListVisibility.Public -> R.drawable.ic_public
|
|
||||||
ListVisibility.Private -> R.drawable.incognito
|
|
||||||
ListVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
|
||||||
},
|
|
||||||
),
|
|
||||||
contentDescription = "Icon for $text List",
|
|
||||||
)
|
|
||||||
Text(text, color = Color.Gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 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.util.Log
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.recalculateWindowInsets
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.PlaylistAddCheck
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListVisibility
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FollowSetsManagementDialog(
|
||||||
|
userHex: String,
|
||||||
|
account: Account,
|
||||||
|
followSetsViewModel: NostrUserListFeedViewModel,
|
||||||
|
navigator: INav,
|
||||||
|
) {
|
||||||
|
val followSetsState by followSetsViewModel.feedContent.collectAsState()
|
||||||
|
val userInfo by remember { derivedStateOf { LocalCache.getOrCreateUser(userHex) } }
|
||||||
|
|
||||||
|
when (followSetsState) {
|
||||||
|
is FollowSetState.Loaded -> {
|
||||||
|
val lists = (followSetsState as FollowSetState.Loaded).feed
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.recalculateWindowInsets(),
|
||||||
|
containerColor = AlertDialogDefaults.containerColor,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Column {
|
||||||
|
Text(text = "Your Lists", fontWeight = FontWeight.SemiBold)
|
||||||
|
// HorizontalDivider()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { navigator.popBack() },
|
||||||
|
) {
|
||||||
|
ArrowBackIcon()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors =
|
||||||
|
TopAppBarDefaults
|
||||||
|
.topAppBarColors(
|
||||||
|
containerColor = AlertDialogDefaults.containerColor,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { contentPadding ->
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(
|
||||||
|
start = 10.dp,
|
||||||
|
end = 10.dp,
|
||||||
|
top = contentPadding.calculateTopPadding(),
|
||||||
|
bottom = contentPadding.calculateBottomPadding(),
|
||||||
|
).consumeWindowInsets(contentPadding)
|
||||||
|
.imePadding(),
|
||||||
|
) {
|
||||||
|
lists.forEachIndexed { index, list ->
|
||||||
|
Spacer(StdVertSpacer)
|
||||||
|
FollowSetItem(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
listHeader = list.title,
|
||||||
|
listVisibility = list.visibility,
|
||||||
|
userName = userInfo.info?.bestName() ?: userInfo.pubkeyDisplayHex(),
|
||||||
|
isUserInList = list.profileList.contains(userHex),
|
||||||
|
onRemoveUser = {
|
||||||
|
Log.d(
|
||||||
|
"Amethyst",
|
||||||
|
"ProfileActions: Removing item from list ...",
|
||||||
|
)
|
||||||
|
followSetsViewModel.removeUserFromSet(
|
||||||
|
userHex,
|
||||||
|
list,
|
||||||
|
account,
|
||||||
|
)
|
||||||
|
Log.d(
|
||||||
|
"Amethyst",
|
||||||
|
"Updated List. New size: ${list.profileList.size}",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onAddUser = {
|
||||||
|
Log.d(
|
||||||
|
"Amethyst",
|
||||||
|
"ProfileActions: Adding item to list ...",
|
||||||
|
)
|
||||||
|
followSetsViewModel.addUserToSet(userHex, list, account)
|
||||||
|
Log.d(
|
||||||
|
"Amethyst",
|
||||||
|
"Updated List. New size: ${list.profileList.size}",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FollowSetItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
listHeader: String,
|
||||||
|
listVisibility: ListVisibility,
|
||||||
|
userName: String,
|
||||||
|
isUserInList: Boolean,
|
||||||
|
onAddUser: () -> Unit,
|
||||||
|
onRemoveUser: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
// .clickable(onClick = onAddUser)
|
||||||
|
.border(
|
||||||
|
width = Dp.Hairline,
|
||||||
|
color = Color.Gray,
|
||||||
|
shape = RoundedCornerShape(percent = 20),
|
||||||
|
).padding(all = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(listHeader, fontWeight = FontWeight.Bold)
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
listVisibility.let {
|
||||||
|
val text by derivedStateOf {
|
||||||
|
when (it) {
|
||||||
|
ListVisibility.Public -> "Public"
|
||||||
|
ListVisibility.Private -> "Private"
|
||||||
|
ListVisibility.Mixed -> "Mixed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
painter =
|
||||||
|
painterResource(
|
||||||
|
when (listVisibility) {
|
||||||
|
ListVisibility.Public -> R.drawable.ic_public
|
||||||
|
ListVisibility.Private -> R.drawable.lock
|
||||||
|
ListVisibility.Mixed -> R.drawable.format_list_bulleted_type
|
||||||
|
},
|
||||||
|
),
|
||||||
|
contentDescription = "Icon for $text List",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = StdVertSpacer)
|
||||||
|
Row {
|
||||||
|
FilterChip(
|
||||||
|
selected = isUserInList,
|
||||||
|
enabled = isUserInList,
|
||||||
|
onClick = {},
|
||||||
|
label = {
|
||||||
|
Text(text = if (isUserInList) "$userName is present in this list" else "$userName is not in this list")
|
||||||
|
},
|
||||||
|
leadingIcon =
|
||||||
|
if (isUserInList) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.PlaylistAddCheck,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
shape = ButtonBorder,
|
||||||
|
)
|
||||||
|
Spacer(modifier = StdHorzSpacer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (isUserInList) onRemoveUser() else onAddUser()
|
||||||
|
},
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.background(
|
||||||
|
color =
|
||||||
|
if (isUserInList) {
|
||||||
|
MaterialTheme.colorScheme.errorContainer
|
||||||
|
} else {
|
||||||
|
ButtonDefaults.filledTonalButtonColors().containerColor
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(percent = 80),
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
if (isUserInList) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text(text = if (isUserInList) "Remove" else "Add", color = Color.Gray)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,34 +20,32 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
|
||||||
|
|
||||||
import android.util.Log
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.List
|
||||||
|
import androidx.compose.material.icons.filled.List
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.observeAccountIsHiddenUser
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.FollowSetState
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps.ShowUserButton
|
||||||
import kotlinx.coroutines.delay
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import kotlinx.coroutines.launch
|
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProfileActions(
|
fun ProfileActions(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
followSetsViewModel: NostrUserListFeedViewModel,
|
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
val followSetsState by followSetsViewModel.feedContent.collectAsState()
|
|
||||||
val (isMenuOpen, setMenuValue) = remember { mutableStateOf(false) }
|
|
||||||
val uiScope = rememberCoroutineScope()
|
|
||||||
val isMe by
|
val isMe by
|
||||||
remember(accountViewModel) { derivedStateOf { accountViewModel.userProfile() == baseUser } }
|
remember(accountViewModel) { derivedStateOf { accountViewModel.userProfile() == baseUser } }
|
||||||
|
|
||||||
@@ -63,31 +61,15 @@ fun ProfileActions(
|
|||||||
DisplayFollowUnfollowButton(baseUser, accountViewModel)
|
DisplayFollowUnfollowButton(baseUser, accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
when (followSetsState) {
|
TextButton(
|
||||||
is FollowSetState.Loaded -> {
|
onClick = { nav.nav(Route.FollowSetManagement(baseUser.pubkeyHex)) },
|
||||||
val lists = (followSetsState as FollowSetState.Loaded).feed
|
shape = ButtonBorder.copy(topStart = CornerSize(0f), bottomStart = CornerSize(0f)),
|
||||||
FollowSetsActionMenu(
|
colors = ButtonDefaults.filledTonalButtonColors(),
|
||||||
isMenuOpen = isMenuOpen,
|
contentPadding = ZeroPadding,
|
||||||
setMenuOpenState = {
|
) {
|
||||||
uiScope.launch {
|
Icon(
|
||||||
delay(100)
|
imageVector = Icons.AutoMirrored.Filled.List,
|
||||||
setMenuValue(!isMenuOpen)
|
contentDescription = "Add or remove user from lists, or create a new list with this user.",
|
||||||
}
|
|
||||||
},
|
|
||||||
userHex = baseUser.pubkeyHex,
|
|
||||||
followLists = lists,
|
|
||||||
addUser = { index, userPubkey, list ->
|
|
||||||
Log.d("Amethyst", "ProfileActions: Updating list ...")
|
|
||||||
followSetsViewModel.addUserToSet(baseUser.pubkeyHex, list, accountViewModel.account)
|
|
||||||
Log.d("Amethyst", "Updated List. New size: ${lists[index].profileList.size}")
|
|
||||||
},
|
|
||||||
removeUser = { index, userPubkey, list ->
|
|
||||||
Log.d("Amethyst", "ProfileActions: Updating list ...")
|
|
||||||
followSetsViewModel.removeUserFromSet(baseUser.pubkeyHex, list, accountViewModel.account)
|
|
||||||
Log.d("Amethyst", "Updated List. New size: ${lists[index].profileList.size}")
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.NostrUserListFeedViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.UserAppRecommendationsFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
@@ -68,7 +67,6 @@ import com.vitorpamplona.amethyst.ui.theme.userProfileBorderModifier
|
|||||||
fun ProfileHeader(
|
fun ProfileHeader(
|
||||||
baseUser: User,
|
baseUser: User,
|
||||||
appRecommendations: UserAppRecommendationsFeedViewModel,
|
appRecommendations: UserAppRecommendationsFeedViewModel,
|
||||||
followSetsViewModel: NostrUserListFeedViewModel,
|
|
||||||
nav: INav,
|
nav: INav,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
@@ -156,7 +154,7 @@ fun ProfileHeader(
|
|||||||
) {
|
) {
|
||||||
MessageButton(baseUser, accountViewModel, nav)
|
MessageButton(baseUser, accountViewModel, nav)
|
||||||
|
|
||||||
ProfileActions(baseUser, followSetsViewModel, accountViewModel, nav)
|
ProfileActions(baseUser, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user