From 24fc44c77fe165428f164b51cfd3f7b6e000f0e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 20:28:05 +0000 Subject: [PATCH 1/3] Group drawer items into collapsible You/Feeds/Create/System sections Separates personal lists (profile, bookmarks, my emoji packs, drafts, wallet, etc.) from discovery feeds (pictures, videos, emoji packs feed, communities, etc.) so the two uses of an emoji icon and the general mix of ownership vs. browsing are no longer visually merged into one flat list. Each section is collapsible via a chevron header but expanded by default for quick access. --- .../ui/navigation/drawer/DrawerContent.kt | 389 ++++++++++-------- amethyst/src/main/res/values/strings.xml | 6 + 2 files changed, 229 insertions(+), 166 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index c422a679a..c1cf3bdea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.navigation.drawer +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -58,6 +59,8 @@ import androidx.compose.material.icons.outlined.AccountBalanceWallet import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.Drafts import androidx.compose.material.icons.outlined.EmojiEmotions +import androidx.compose.material.icons.outlined.ExpandLess +import androidx.compose.material.icons.outlined.ExpandMore import androidx.compose.material.icons.outlined.GroupAdd import androidx.compose.material.icons.outlined.Groups import androidx.compose.material.icons.outlined.Language @@ -124,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Font18SP import com.vitorpamplona.amethyst.ui.theme.IconRowTextModifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier @@ -538,179 +542,187 @@ fun ListContent( nav: INav, ) { Column(modifier) { - NavigationRow( - title = R.string.profile, - icon = Icons.Default.AccountCircle, - tint = MaterialTheme.colorScheme.primary, - nav = nav, - computeRoute = { - Route.Profile(accountViewModel.userProfile().pubkeyHex) - }, - ) - - NavigationRow( - title = R.string.my_lists, - icon = Icons.AutoMirrored.Filled.FormatListBulleted, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Lists, - ) - - NavigationRow( - title = R.string.bookmarks, - icon = Icons.Outlined.CollectionsBookmark, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.BookmarkGroups, - ) - - NavigationRow( - title = R.string.manage_emoji_packs, - icon = Icons.Outlined.EmojiEmotions, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.EmojiPacks, - ) - - NavigationRow( - title = R.string.interest_sets_title, - icon = Icons.Outlined.Tag, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.InterestSets, - ) - - NavigationRow( - title = R.string.web_bookmarks, - icon = Icons.Outlined.Language, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.WebBookmarks, - ) - - NavigationRow( - title = R.string.drafts, - icon = Icons.Outlined.Drafts, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Drafts, - ) - - NavigationRow( - title = R.string.badges, - icon = Icons.Outlined.MilitaryTech, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Badges, - ) - - NavigationRow( - title = R.string.communities, - icon = Icons.Outlined.Groups, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Communities, - ) - - NavigationRow( - title = R.string.discover_marketplace, - icon = Icons.Outlined.Storefront, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Products, - ) - - NavigationRow( - title = R.string.pictures, - icon = Icons.Outlined.Photo, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Pictures, - ) - - NavigationRow( - title = R.string.polls, - icon = R.drawable.ic_poll, - iconReference = 1, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Polls, - ) - - NavigationRow( - title = R.string.discover_reads, - icon = Icons.AutoMirrored.Outlined.Article, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Articles, - ) - - NavigationRow( - title = R.string.shorts, - icon = Icons.Outlined.PlayCircle, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Shorts, - ) - - NavigationRow( - title = R.string.longs, - icon = Icons.Outlined.SmartDisplay, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Longs, - ) - - NavigationRow( - title = R.string.wallet, - icon = Icons.Outlined.AccountBalanceWallet, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.Wallet, - ) - - NavigationRow( - title = R.string.emoji_sets, - icon = Icons.Outlined.EmojiEmotions, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.BrowseEmojiSets, - ) - - NavigationRow( - title = R.string.share_hls_video, - icon = Icons.Outlined.SettingsInputAntenna, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.NewHlsVideo, - ) - - if (isDebug) { + CollapsibleSection(title = R.string.drawer_section_you) { NavigationRow( - title = R.string.route_chess, - icon = R.drawable.ic_chess, - iconReference = 1, + title = R.string.profile, + icon = Icons.Default.AccountCircle, + tint = MaterialTheme.colorScheme.primary, + nav = nav, + computeRoute = { + Route.Profile(accountViewModel.userProfile().pubkeyHex) + }, + ) + + NavigationRow( + title = R.string.my_lists, + icon = Icons.AutoMirrored.Filled.FormatListBulleted, tint = MaterialTheme.colorScheme.onBackground, nav = nav, - route = Route.Chess, + route = Route.Lists, + ) + + NavigationRow( + title = R.string.bookmarks, + icon = Icons.Outlined.CollectionsBookmark, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.BookmarkGroups, + ) + + NavigationRow( + title = R.string.web_bookmarks, + icon = Icons.Outlined.Language, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.WebBookmarks, + ) + + NavigationRow( + title = R.string.drafts, + icon = Icons.Outlined.Drafts, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Drafts, + ) + + NavigationRow( + title = R.string.interest_sets_title, + icon = Icons.Outlined.Tag, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.InterestSets, + ) + + NavigationRow( + title = R.string.manage_emoji_packs, + icon = Icons.Outlined.EmojiEmotions, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.EmojiPacks, + ) + + NavigationRow( + title = R.string.wallet, + icon = Icons.Outlined.AccountBalanceWallet, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Wallet, ) } - IconRowRelays( - accountViewModel = accountViewModel, - onClick = { - nav.closeDrawer() - nav.nav(Route.EditRelays) - }, - ) + CollapsibleSection(title = R.string.drawer_section_feeds) { + NavigationRow( + title = R.string.communities, + icon = Icons.Outlined.Groups, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Communities, + ) - NavigationRow( - title = R.string.settings, - icon = Icons.Outlined.Settings, - tint = MaterialTheme.colorScheme.onBackground, - nav = nav, - route = Route.AllSettings, - ) + NavigationRow( + title = R.string.discover_reads, + icon = Icons.AutoMirrored.Outlined.Article, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Articles, + ) + + NavigationRow( + title = R.string.pictures, + icon = Icons.Outlined.Photo, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Pictures, + ) + + NavigationRow( + title = R.string.shorts, + icon = Icons.Outlined.PlayCircle, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Shorts, + ) + + NavigationRow( + title = R.string.longs, + icon = Icons.Outlined.SmartDisplay, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Longs, + ) + + NavigationRow( + title = R.string.polls, + icon = R.drawable.ic_poll, + iconReference = 1, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Polls, + ) + + NavigationRow( + title = R.string.badges, + icon = Icons.Outlined.MilitaryTech, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Badges, + ) + + NavigationRow( + title = R.string.discover_marketplace, + icon = Icons.Outlined.Storefront, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Products, + ) + + NavigationRow( + title = R.string.emoji_sets, + icon = Icons.Outlined.EmojiEmotions, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.BrowseEmojiSets, + ) + } + + CollapsibleSection(title = R.string.drawer_section_create) { + NavigationRow( + title = R.string.share_hls_video, + icon = Icons.Outlined.SettingsInputAntenna, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.NewHlsVideo, + ) + + if (isDebug) { + NavigationRow( + title = R.string.route_chess, + icon = R.drawable.ic_chess, + iconReference = 1, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.Chess, + ) + } + } + + CollapsibleSection(title = R.string.drawer_section_system) { + IconRowRelays( + accountViewModel = accountViewModel, + onClick = { + nav.closeDrawer() + nav.nav(Route.EditRelays) + }, + ) + + NavigationRow( + title = R.string.settings, + icon = Icons.Outlined.Settings, + tint = MaterialTheme.colorScheme.onBackground, + nav = nav, + route = Route.AllSettings, + ) + } Spacer(modifier = Modifier.weight(1f)) @@ -723,6 +735,51 @@ fun ListContent( } } +@Composable +fun CollapsibleSection( + title: Int, + content: @Composable () -> Unit, +) { + var expanded by remember { mutableStateOf(true) } + val sectionTitle = stringRes(title) + val toggleLabel = + stringRes( + if (expanded) R.string.drawer_collapse_section else R.string.drawer_expand_section, + sectionTitle, + ) + + Column(modifier = Modifier.animateContentSize()) { + Row( + modifier = + Modifier + .fillMaxWidth() + .clickable( + onClick = { expanded = !expanded }, + onClickLabel = toggleLabel, + ).padding(top = 12.dp, bottom = 4.dp, start = 25.dp, end = 15.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = sectionTitle.uppercase(), + fontSize = Font14SP, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Icon( + imageVector = if (expanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore, + contentDescription = null, + modifier = Size22Modifier, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + if (expanded) { + content() + } + } +} + @Composable fun NavigationRow( title: Int, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 020f7908a..4ff105b8e 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -369,6 +369,12 @@ "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." Add New Account Accounts + You + Feeds + Create + System + Expand %1$s section + Collapse %1$s section Select Account Add New Account Active account From 60c046ed691384e234fcd608c4acfb5f1eeda888 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 20:47:16 +0000 Subject: [PATCH 2/3] Hoist drawer section header modifier and drop per-frame allocations CollapsibleSection was re-allocating a Modifier chain, uppercasing the title String, and formatting an accessibility label on every recomposition. Move the static padding/fillMaxWidth into DrawerSectionHeaderModifier in Shape.kt, use the section title as-is, and rely on the chevron's contentDescription instead of a per-frame onClickLabel format call. --- .../ui/navigation/drawer/DrawerContent.kt | 18 ++++-------------- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 1 + amethyst/src/main/res/values/strings.xml | 2 -- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index c1cf3bdea..f80e85180 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -127,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer +import com.vitorpamplona.amethyst.ui.theme.DrawerSectionHeaderModifier import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Font18SP import com.vitorpamplona.amethyst.ui.theme.IconRowTextModifier @@ -742,33 +743,22 @@ fun CollapsibleSection( ) { var expanded by remember { mutableStateOf(true) } val sectionTitle = stringRes(title) - val toggleLabel = - stringRes( - if (expanded) R.string.drawer_collapse_section else R.string.drawer_expand_section, - sectionTitle, - ) Column(modifier = Modifier.animateContentSize()) { Row( - modifier = - Modifier - .fillMaxWidth() - .clickable( - onClick = { expanded = !expanded }, - onClickLabel = toggleLabel, - ).padding(top = 12.dp, bottom = 4.dp, start = 25.dp, end = 15.dp), + modifier = DrawerSectionHeaderModifier.clickable { expanded = !expanded }, verticalAlignment = Alignment.CenterVertically, ) { Text( modifier = Modifier.weight(1f), - text = sectionTitle.uppercase(), + text = sectionTitle, fontSize = Font14SP, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant, ) Icon( imageVector = if (expanded) Icons.Outlined.ExpandLess else Icons.Outlined.ExpandMore, - contentDescription = null, + contentDescription = sectionTitle, modifier = Size22Modifier, tint = MaterialTheme.colorScheme.onSurfaceVariant, ) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 3096570b8..940990b44 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -263,6 +263,7 @@ val drawerSpacing = Modifier.padding(top = Size10dp, start = Size25dp, end = Siz val IconRowTextModifier = Modifier.padding(start = 16.dp) val IconRowModifier = Modifier.fillMaxWidth().padding(vertical = 15.dp, horizontal = 25.dp) +val DrawerSectionHeaderModifier = Modifier.fillMaxWidth().padding(top = 12.dp, bottom = 4.dp, start = 25.dp, end = 15.dp) val Width16Space = Modifier.width(Size16dp) diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 4ff105b8e..963970ed1 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -373,8 +373,6 @@ Feeds Create System - Expand %1$s section - Collapse %1$s section Select Account Add New Account Active account From bf28fbf26fbea45b1a58520acb395551ad576223 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Apr 2026 20:53:11 +0000 Subject: [PATCH 3/3] Add @Preview composables for CollapsibleSection and ListContent CollapsibleSectionPreview shows two sections with representative rows so header styling, expand state, and chevron alignment can be verified against light/dark themes. ListContentPreview wires mockAccountViewModel and EmptyNav to render the whole reorganized drawer body. --- .../ui/navigation/drawer/DrawerContent.kt | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt index f80e85180..cebcfd271 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/drawer/DrawerContent.kt @@ -103,6 +103,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withLink +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -119,11 +120,13 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUse import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserStatuses import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel +import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.DividerThickness import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer @@ -138,6 +141,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.Size26Modifier import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.TextStyleBottomNavBar +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.Width16Space import com.vitorpamplona.amethyst.ui.theme.bannerModifier import com.vitorpamplona.amethyst.ui.theme.drawerSpacing @@ -1017,3 +1021,59 @@ fun BottomContent( } } } + +@Preview +@Composable +private fun CollapsibleSectionPreview() { + ThemeComparisonColumn { + Column { + CollapsibleSection(title = R.string.drawer_section_you) { + IconRow( + title = R.string.profile, + icon = Icons.Default.AccountCircle, + tint = MaterialTheme.colorScheme.primary, + onClick = {}, + ) + IconRow( + title = R.string.bookmarks, + icon = Icons.Outlined.CollectionsBookmark, + tint = MaterialTheme.colorScheme.onBackground, + onClick = {}, + ) + IconRow( + title = R.string.drafts, + icon = Icons.Outlined.Drafts, + tint = MaterialTheme.colorScheme.onBackground, + onClick = {}, + ) + } + CollapsibleSection(title = R.string.drawer_section_feeds) { + IconRow( + title = R.string.pictures, + icon = Icons.Outlined.Photo, + tint = MaterialTheme.colorScheme.onBackground, + onClick = {}, + ) + IconRow( + title = R.string.longs, + icon = Icons.Outlined.SmartDisplay, + tint = MaterialTheme.colorScheme.onBackground, + onClick = {}, + ) + } + } + } +} + +@Preview(widthDp = 320, heightDp = 1400) +@Composable +private fun ListContentPreview() { + ThemeComparisonColumn { + ListContent( + modifier = Modifier.fillMaxWidth(), + openSheet = {}, + accountViewModel = mockAccountViewModel(), + nav = EmptyNav(), + ) + } +}