From bfd3ddaca6b815e34d89246695ab99f9670f657e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Apr 2026 03:29:41 +0000 Subject: [PATCH] feat(home): user-configurable home tabs (new threads, conversations, everything) Adds a Home Tabs settings screen letting users pick which tabs appear on Home: New Threads, Conversations, and a new combined Everything tab. The tab row is hidden whenever only one tab is active, so users can keep a single feed view. At least one tab always remains active. --- .../amethyst/model/UiSettings.kt | 3 + .../amethyst/model/UiSettingsFlow.kt | 27 +++ .../ui/feeds/RememberForeverStates.kt | 1 + .../amethyst/ui/navigation/AppNavigation.kt | 2 + .../amethyst/ui/navigation/routes/Routes.kt | 2 + .../loggedIn/AccountFeedContentStates.kt | 4 + .../ui/screen/loggedIn/home/HomeScreen.kt | 114 ++++++++---- .../home/dal/HomeEverythingFeedFilter.kt | 61 +++++++ .../loggedIn/settings/AllSettingsScreen.kt | 7 + .../settings/HomeTabsSettingsScreen.kt | 168 ++++++++++++++++++ amethyst/src/main/res/values/strings.xml | 3 + 11 files changed, 359 insertions(+), 33 deletions(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeEverythingFeedFilter.kt create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt index 154bd40fd6..e1ebf5fa8c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettings.kt @@ -44,6 +44,9 @@ data class UiSettings( val automaticallyProposeAiImprovements: BooleanType = BooleanType.ALWAYS, val useTrackedBroadcasts: BooleanType = BooleanType.ALWAYS, val bottomBarItems: List = DefaultBottomBarItems, + val showHomeNewThreadsTab: Boolean = true, + val showHomeConversationsTab: Boolean = true, + val showHomeEverythingTab: Boolean = false, ) enum class ThemeType( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt index b7bcb17c34..8a57956362 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/UiSettingsFlow.kt @@ -44,6 +44,9 @@ class UiSettingsFlow( val automaticallyProposeAiImprovements: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), val useTrackedBroadcasts: MutableStateFlow = MutableStateFlow(BooleanType.ALWAYS), val bottomBarItems: MutableStateFlow> = MutableStateFlow(DefaultBottomBarItems), + val showHomeNewThreadsTab: MutableStateFlow = MutableStateFlow(true), + val showHomeConversationsTab: MutableStateFlow = MutableStateFlow(true), + val showHomeEverythingTab: MutableStateFlow = MutableStateFlow(false), ) { val listOfFlows: List> = listOf>( @@ -62,6 +65,9 @@ class UiSettingsFlow( automaticallyProposeAiImprovements, useTrackedBroadcasts, bottomBarItems, + showHomeNewThreadsTab, + showHomeConversationsTab, + showHomeEverythingTab, ) // emits at every change in any of the propertyes. @@ -84,6 +90,9 @@ class UiSettingsFlow( flows[12] as BooleanType, flows[13] as BooleanType, flows[14] as List, + flows[15] as Boolean, + flows[16] as Boolean, + flows[17] as Boolean, ) } @@ -104,6 +113,9 @@ class UiSettingsFlow( automaticallyProposeAiImprovements.value, useTrackedBroadcasts.value, bottomBarItems.value, + showHomeNewThreadsTab.value, + showHomeConversationsTab.value, + showHomeEverythingTab.value, ) fun update(torSettings: UiSettings): Boolean { @@ -169,6 +181,18 @@ class UiSettingsFlow( bottomBarItems.tryEmit(torSettings.bottomBarItems) any = true } + if (showHomeNewThreadsTab.value != torSettings.showHomeNewThreadsTab) { + showHomeNewThreadsTab.tryEmit(torSettings.showHomeNewThreadsTab) + any = true + } + if (showHomeConversationsTab.value != torSettings.showHomeConversationsTab) { + showHomeConversationsTab.tryEmit(torSettings.showHomeConversationsTab) + any = true + } + if (showHomeEverythingTab.value != torSettings.showHomeEverythingTab) { + showHomeEverythingTab.tryEmit(torSettings.showHomeEverythingTab) + any = true + } return any } @@ -203,6 +227,9 @@ class UiSettingsFlow( MutableStateFlow(uiSettings.automaticallyProposeAiImprovements), MutableStateFlow(uiSettings.useTrackedBroadcasts), MutableStateFlow(uiSettings.bottomBarItems), + MutableStateFlow(uiSettings.showHomeNewThreadsTab), + MutableStateFlow(uiSettings.showHomeConversationsTab), + MutableStateFlow(uiSettings.showHomeEverythingTab), ) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt index 4528457987..c5d0794630 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/feeds/RememberForeverStates.kt @@ -41,6 +41,7 @@ object ScrollStateKeys { const val VIDEO_SCREEN = "VideoFeed" const val HOME_FOLLOWS = "HomeFollowsFeed" const val HOME_REPLIES = "HomeFollowsRepliesFeed" + const val HOME_EVERYTHING = "HomeFollowsEverythingFeed" const val MESSAGES_KNOWN = "MessagesKnown" const val MESSAGES_NEW = "MessagesNew" const val PROFILE_GALLERY = "ProfileGalleryFeed" diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index 69c66edcfc..4d6aaa5016 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -154,6 +154,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.AllSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.BottomBarSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.CallSettingsScreen +import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.HomeTabsSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen @@ -305,6 +306,7 @@ fun BuildNavigation( composableFromEnd { UserSettingsScreen(accountViewModel, nav) } composableFromEnd { ReactionsSettingsScreen(accountViewModel, nav) } composableFromEnd { BottomBarSettingsScreen(accountViewModel, nav) } + composableFromEnd { HomeTabsSettingsScreen(accountViewModel, nav) } composableFromEnd { VideoPlayerSettingsScreen(accountViewModel, nav) } composableFromEnd { CallSettingsScreen(accountViewModel, nav) } composableFromEnd { ImportFollowListSelectUserScreen(accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index bf8742969a..62649f251c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -213,6 +213,8 @@ sealed class Route { @Serializable object BottomBarSettings : Route() + @Serializable object HomeTabsSettings : Route() + @Serializable object VideoPlayerSettings : Route() @Serializable object CallSettings : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt index 8c9e821b9e..8280958560 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountFeedContentStates.kt @@ -43,6 +43,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.drafts.dal.DraftEventsFeedF import com.vitorpamplona.amethyst.ui.screen.loggedIn.emojipacks.browse.dal.BrowseEmojiSetsFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.dal.FollowPacksFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeConversationsFeedFilter +import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeEverythingFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeLiveFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.dal.LiveStreamsFeedFilter @@ -72,6 +73,7 @@ class AccountFeedContentStates( val homeLive = ChannelFeedContentState(HomeLiveFilter(account), scope) val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope, LocalCache) val homeReplies = FeedContentState(HomeConversationsFeedFilter(account), scope, LocalCache) + val homeEverything = FeedContentState(HomeEverythingFeedFilter(account), scope, LocalCache) val dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope, LocalCache) val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope, LocalCache) @@ -139,6 +141,7 @@ class AccountFeedContentStates( homeLive.updateFeedWith(newNotes) homeNewThreads.updateFeedWith(newNotes) homeReplies.updateFeedWith(newNotes) + homeEverything.updateFeedWith(newNotes) dmKnown.updateFeedWith(newNotes) dmNew.updateFeedWith(newNotes) @@ -186,6 +189,7 @@ class AccountFeedContentStates( homeLive.deleteFromFeed(newNotes) homeNewThreads.deleteFromFeed(newNotes) homeReplies.deleteFromFeed(newNotes) + homeEverything.deleteFromFeed(newNotes) dmKnown.updateFeedWith(newNotes) dmNew.updateFeedWith(newNotes) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt index 03c88f35b6..cb69b582ce 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/HomeScreen.kt @@ -108,6 +108,7 @@ fun HomeScreen( liveFeedState = accountViewModel.feedStates.homeLive, newThreadsFeedState = accountViewModel.feedStates.homeNewThreads, repliesFeedState = accountViewModel.feedStates.homeReplies, + everythingFeedState = accountViewModel.feedStates.homeEverything, accountViewModel = accountViewModel, nav = nav, ) @@ -119,18 +120,20 @@ fun HomeScreen( liveFeedState: ChannelFeedContentState, newThreadsFeedState: FeedContentState, repliesFeedState: FeedContentState, + everythingFeedState: FeedContentState, accountViewModel: AccountViewModel, nav: INav, ) { - WatchAccountForHomeScreen(liveFeedState, newThreadsFeedState, repliesFeedState, accountViewModel) + WatchAccountForHomeScreen(liveFeedState, newThreadsFeedState, repliesFeedState, everythingFeedState, accountViewModel) WatchLifecycleAndUpdateModel(liveFeedState) WatchLifecycleAndUpdateModel(newThreadsFeedState) WatchLifecycleAndUpdateModel(repliesFeedState) + WatchLifecycleAndUpdateModel(everythingFeedState) HomeFilterAssemblerSubscription(accountViewModel) - AssembleHomeTabs(newThreadsFeedState, repliesFeedState, liveFeedState) { pagerState, tabItems -> + AssembleHomeTabs(newThreadsFeedState, repliesFeedState, everythingFeedState, liveFeedState, accountViewModel) { pagerState, tabItems -> HomePages(pagerState, tabItems, accountViewModel, nav) } } @@ -140,33 +143,74 @@ fun HomeScreen( private fun AssembleHomeTabs( newThreadsFeedState: FeedContentState, repliesFeedState: FeedContentState, + everythingFeedState: FeedContentState, liveFeedState: ChannelFeedContentState, + accountViewModel: AccountViewModel, inner: @Composable (PagerState, ImmutableList) -> Unit, ) { - val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN) { 2 } + val showNewThreads by accountViewModel.settings.uiSettingsFlow.showHomeNewThreadsTab + .collectAsStateWithLifecycle() + val showConversations by accountViewModel.settings.uiSettingsFlow.showHomeConversationsTab + .collectAsStateWithLifecycle() + val showEverything by accountViewModel.settings.uiSettingsFlow.showHomeEverythingTab + .collectAsStateWithLifecycle() val tabs by - remember(newThreadsFeedState, repliesFeedState) { + remember(newThreadsFeedState, repliesFeedState, everythingFeedState, showNewThreads, showConversations, showEverything) { mutableStateOf( - listOf( - TabItem( - resource = R.string.new_threads, - feedState = newThreadsFeedState, - routeForLastRead = "HomeFollows", - scrollStateKey = ScrollStateKeys.HOME_FOLLOWS, - liveSection = liveFeedState, - ), - TabItem( - resource = R.string.conversations, - feedState = repliesFeedState, - routeForLastRead = "HomeFollowsReplies", - scrollStateKey = ScrollStateKeys.HOME_REPLIES, - liveSection = liveFeedState, - ), - ).toImmutableList(), + buildList { + if (showNewThreads) { + add( + TabItem( + resource = R.string.new_threads, + feedState = newThreadsFeedState, + routeForLastRead = "HomeFollows", + scrollStateKey = ScrollStateKeys.HOME_FOLLOWS, + liveSection = liveFeedState, + ), + ) + } + if (showConversations) { + add( + TabItem( + resource = R.string.conversations, + feedState = repliesFeedState, + routeForLastRead = "HomeFollowsReplies", + scrollStateKey = ScrollStateKeys.HOME_REPLIES, + liveSection = liveFeedState, + ), + ) + } + if (showEverything) { + add( + TabItem( + resource = R.string.home_tab_everything, + feedState = everythingFeedState, + routeForLastRead = "HomeFollowsEverything", + scrollStateKey = ScrollStateKeys.HOME_EVERYTHING, + liveSection = liveFeedState, + ), + ) + } + // Always render at least one tab so the screen doesn't go blank + // if the user disables every option. + if (isEmpty()) { + add( + TabItem( + resource = R.string.new_threads, + feedState = newThreadsFeedState, + routeForLastRead = "HomeFollows", + scrollStateKey = ScrollStateKeys.HOME_FOLLOWS, + liveSection = liveFeedState, + ), + ) + } + }.toImmutableList(), ) } + val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN) { tabs.size } + inner(pagerState, tabs) } @@ -182,19 +226,21 @@ private fun HomePages( topBar = { Column { HomeTopBar(accountViewModel, nav) - SecondaryTabRow( - containerColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.onBackground, - modifier = TabRowHeight, - selectedTabIndex = pagerState.currentPage, - ) { - val coroutineScope = rememberCoroutineScope() - tabs.forEachIndexed { index, tab -> - Tab( - selected = pagerState.currentPage == index, - text = { Text(text = stringRes(tab.resource)) }, - onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } }, - ) + if (tabs.size > 1) { + SecondaryTabRow( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + modifier = TabRowHeight, + selectedTabIndex = pagerState.currentPage, + ) { + val coroutineScope = rememberCoroutineScope() + tabs.forEachIndexed { index, tab -> + Tab( + selected = pagerState.currentPage == index, + text = { Text(text = stringRes(tab.resource)) }, + onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } }, + ) + } } } } @@ -506,6 +552,7 @@ fun WatchAccountForHomeScreen( liveFeedState: ChannelFeedContentState, newThreadsFeedState: FeedContentState, repliesFeedState: FeedContentState, + everythingFeedState: FeedContentState, accountViewModel: AccountViewModel, ) { val homeFollowList by accountViewModel.account.liveHomeFollowLists.collectAsStateWithLifecycle() @@ -513,6 +560,7 @@ fun WatchAccountForHomeScreen( LaunchedEffect(accountViewModel, homeFollowList) { newThreadsFeedState.checkKeysInvalidateDataAndSendToTop() repliesFeedState.checkKeysInvalidateDataAndSendToTop() + everythingFeedState.checkKeysInvalidateDataAndSendToTop() liveFeedState.checkKeysInvalidateDataAndSendToTop() } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeEverythingFeedFilter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeEverythingFeedFilter.kt new file mode 100644 index 0000000000..fd4f9966b8 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/dal/HomeEverythingFeedFilter.kt @@ -0,0 +1,61 @@ +/* + * 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.home.dal + +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByOutboxTopNavFilter +import com.vitorpamplona.amethyst.model.topNavFeeds.noteBased.muted.MutedAuthorsByProxyTopNavFilter +import com.vitorpamplona.amethyst.ui.dal.AdditiveFeedFilter +import com.vitorpamplona.amethyst.ui.dal.DefaultFeedOrder +import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent +import com.vitorpamplona.quartz.nip18Reposts.RepostEvent + +/** + * Combined home feed: every event accepted by either the New Threads filter + * or the Conversations (replies) filter, in one list. + */ +class HomeEverythingFeedFilter( + val account: Account, +) : AdditiveFeedFilter() { + private val newThreads = HomeNewThreadFeedFilter(account) + private val conversations = HomeConversationsFeedFilter(account) + + override fun feedKey(): String = account.userProfile().pubkeyHex + "-" + account.settings.defaultHomeFollowList.value + + override fun showHiddenKey(): Boolean = + account.liveHomeFollowLists.value is MutedAuthorsByOutboxTopNavFilter || + account.liveHomeFollowLists.value is MutedAuthorsByProxyTopNavFilter + + override fun feed(): List = sort((newThreads.feed() + conversations.feed()).toSet()) + + override fun applyFilter(newItems: Set): Set = newThreads.applyFilter(newItems) + conversations.applyFilter(newItems) + + override fun sort(items: Set): List = + items + .distinctBy { + if (it.event is RepostEvent || it.event is GenericRepostEvent) { + it.replyTo?.lastOrNull()?.idHex ?: it.idHex + } else { + it.idHex + } + }.sortedWith(DefaultFeedOrder) +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt index 6f389d5d6f..2c12332730 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AllSettingsScreen.kt @@ -251,6 +251,13 @@ fun AllSettingsScreen( tint = tint, onClick = { nav.nav(Route.BottomBarSettings) }, ) + HorizontalDivider() + SettingsNavigationRow( + title = R.string.home_tabs_settings, + icon = MaterialSymbols.Home, + tint = tint, + onClick = { nav.nav(Route.HomeTabsSettings) }, + ) HorizontalDivider(thickness = 4.dp) SettingsSectionHeader(R.string.danger_zone) accountViewModel.account.settings.keyPair.privKey?.let { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt new file mode 100644 index 0000000000..521e117ac3 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/HomeTabsSettingsScreen.kt @@ -0,0 +1,168 @@ +/* + * 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.settings + +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.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav +import com.vitorpamplona.amethyst.ui.navigation.navs.INav +import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton +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.Size20dp +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow + +@Preview +@Composable +fun HomeTabsSettingsScreenPreview() { + ThemeComparisonRow { + HomeTabsSettingsScreen( + mockAccountViewModel(), + EmptyNav(), + ) + } +} + +@Composable +fun HomeTabsSettingsScreen( + accountViewModel: AccountViewModel, + nav: INav, +) { + Scaffold( + topBar = { + TopBarWithBackButton(stringRes(id = R.string.home_tabs_settings), nav) + }, + ) { padding -> + Column(Modifier.padding(padding)) { + HomeTabsSettingsContent(accountViewModel) + } + } +} + +@Composable +fun HomeTabsSettingsContent(accountViewModel: AccountViewModel) { + val ui = accountViewModel.settings.uiSettingsFlow + + val showNewThreads by ui.showHomeNewThreadsTab.collectAsStateWithLifecycle() + val showConversations by ui.showHomeConversationsTab.collectAsStateWithLifecycle() + val showEverything by ui.showHomeEverythingTab.collectAsStateWithLifecycle() + + val activeCount = listOf(showNewThreads, showConversations, showEverything).count { it } + + Column( + modifier = + Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + ) { + Spacer(Modifier.height(16.dp)) + + Text( + text = stringRes(R.string.home_tabs_settings_description), + style = MaterialTheme.typography.bodyMedium, + color = Color.Gray, + modifier = Modifier.padding(bottom = 16.dp, start = Size20dp, end = Size20dp), + ) + + HomeTabSwitchRow( + title = stringRes(R.string.new_threads), + checked = showNewThreads, + // Don't allow disabling the last remaining tab. + enabled = !(showNewThreads && activeCount == 1), + onCheckedChange = { + ui.showHomeNewThreadsTab.tryEmit(it) + }, + ) + HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp)) + + HomeTabSwitchRow( + title = stringRes(R.string.conversations), + checked = showConversations, + enabled = !(showConversations && activeCount == 1), + onCheckedChange = { + ui.showHomeConversationsTab.tryEmit(it) + }, + ) + HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp)) + + HomeTabSwitchRow( + title = stringRes(R.string.home_tab_everything), + checked = showEverything, + enabled = !(showEverything && activeCount == 1), + onCheckedChange = { + ui.showHomeEverythingTab.tryEmit(it) + }, + ) + + Spacer(Modifier.height(16.dp)) + } +} + +@Composable +private fun HomeTabSwitchRow( + title: String, + checked: Boolean, + enabled: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 12.dp, horizontal = Size20dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + Switch( + checked = checked, + enabled = enabled, + onCheckedChange = onCheckedChange, + ) + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 0567dec430..a2e65e8dda 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1726,6 +1726,9 @@ Drag to reorder. Toggle to add or remove an item from the bottom bar. With zero items the bottom bar is hidden. Available Reorder + Home Tabs + Pick which tabs appear on the Home screen. When only one tab is active the tab bar is hidden. + Everything Reaction Row Configure which reaction buttons are shown, their order, and whether to display counters. Enabled