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:
Vitor Pamplona
2026-04-30 08:51:02 -04:00
committed by GitHub
11 changed files with 359 additions and 33 deletions
@@ -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()
@@ -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)
@@ -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()
}
}
@@ -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)
}
@@ -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 {
@@ -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,
)
}
}
+3
View File
@@ -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>