Merge pull request #2654 from vitorpamplona/claude/add-tag-filters-YR1wO
feat(home): user-configurable home tabs (new threads, conversations, everything)
This commit is contained in:
@@ -44,6 +44,9 @@ data class UiSettings(
|
||||
val automaticallyProposeAiImprovements: BooleanType = BooleanType.ALWAYS,
|
||||
val useTrackedBroadcasts: BooleanType = BooleanType.ALWAYS,
|
||||
val bottomBarItems: List<NavBarItem> = DefaultBottomBarItems,
|
||||
val showHomeNewThreadsTab: Boolean = true,
|
||||
val showHomeConversationsTab: Boolean = true,
|
||||
val showHomeEverythingTab: Boolean = false,
|
||||
)
|
||||
|
||||
enum class ThemeType(
|
||||
|
||||
@@ -44,6 +44,9 @@ class UiSettingsFlow(
|
||||
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
val useTrackedBroadcasts: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||
val bottomBarItems: MutableStateFlow<List<NavBarItem>> = MutableStateFlow(DefaultBottomBarItems),
|
||||
val showHomeNewThreadsTab: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||
val showHomeConversationsTab: MutableStateFlow<Boolean> = MutableStateFlow(true),
|
||||
val showHomeEverythingTab: MutableStateFlow<Boolean> = MutableStateFlow(false),
|
||||
) {
|
||||
val listOfFlows: List<Flow<Any?>> =
|
||||
listOf<Flow<Any?>>(
|
||||
@@ -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<NavBarItem>,
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.BottomBarSettings> { BottomBarSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.HomeTabsSettings> { HomeTabsSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.VideoPlayerSettings> { VideoPlayerSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.CallSettings> { CallSettingsScreen(accountViewModel, nav) }
|
||||
composableFromEnd<Route.ImportFollowsSelectUser> { ImportFollowListSelectUserScreen(accountViewModel, nav) }
|
||||
|
||||
@@ -213,6 +213,8 @@ sealed class Route {
|
||||
|
||||
@Serializable object BottomBarSettings : Route()
|
||||
|
||||
@Serializable object HomeTabsSettings : Route()
|
||||
|
||||
@Serializable object VideoPlayerSettings : Route()
|
||||
|
||||
@Serializable object CallSettings : Route()
|
||||
|
||||
+4
@@ -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)
|
||||
|
||||
+81
-33
@@ -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<TabItem>) -> 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()
|
||||
}
|
||||
}
|
||||
|
||||
+61
@@ -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<Note>() {
|
||||
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<Note> = sort((newThreads.feed() + conversations.feed()).toSet())
|
||||
|
||||
override fun applyFilter(newItems: Set<Note>): Set<Note> = newThreads.applyFilter(newItems) + conversations.applyFilter(newItems)
|
||||
|
||||
override fun sort(items: Set<Note>): List<Note> =
|
||||
items
|
||||
.distinctBy {
|
||||
if (it.event is RepostEvent || it.event is GenericRepostEvent) {
|
||||
it.replyTo?.lastOrNull()?.idHex ?: it.idHex
|
||||
} else {
|
||||
it.idHex
|
||||
}
|
||||
}.sortedWith(DefaultFeedOrder)
|
||||
}
|
||||
+7
@@ -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 {
|
||||
|
||||
+168
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1726,6 +1726,9 @@
|
||||
<string name="bottom_bar_settings_description">Drag to reorder. Toggle to add or remove an item from the bottom bar. With zero items the bottom bar is hidden.</string>
|
||||
<string name="bottom_bar_settings_available">Available</string>
|
||||
<string name="bottom_bar_settings_reorder">Reorder</string>
|
||||
<string name="home_tabs_settings">Home Tabs</string>
|
||||
<string name="home_tabs_settings_description">Pick which tabs appear on the Home screen. When only one tab is active the tab bar is hidden.</string>
|
||||
<string name="home_tab_everything">Everything</string>
|
||||
<string name="reactions_settings">Reaction Row</string>
|
||||
<string name="reactions_settings_description">Configure which reaction buttons are shown, their order, and whether to display counters.</string>
|
||||
<string name="reactions_settings_enabled">Enabled</string>
|
||||
|
||||
Reference in New Issue
Block a user