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 automaticallyProposeAiImprovements: BooleanType = BooleanType.ALWAYS,
|
||||||
val useTrackedBroadcasts: BooleanType = BooleanType.ALWAYS,
|
val useTrackedBroadcasts: BooleanType = BooleanType.ALWAYS,
|
||||||
val bottomBarItems: List<NavBarItem> = DefaultBottomBarItems,
|
val bottomBarItems: List<NavBarItem> = DefaultBottomBarItems,
|
||||||
|
val showHomeNewThreadsTab: Boolean = true,
|
||||||
|
val showHomeConversationsTab: Boolean = true,
|
||||||
|
val showHomeEverythingTab: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class ThemeType(
|
enum class ThemeType(
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ class UiSettingsFlow(
|
|||||||
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
val automaticallyProposeAiImprovements: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||||
val useTrackedBroadcasts: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
val useTrackedBroadcasts: MutableStateFlow<BooleanType> = MutableStateFlow(BooleanType.ALWAYS),
|
||||||
val bottomBarItems: MutableStateFlow<List<NavBarItem>> = MutableStateFlow(DefaultBottomBarItems),
|
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?>> =
|
val listOfFlows: List<Flow<Any?>> =
|
||||||
listOf<Flow<Any?>>(
|
listOf<Flow<Any?>>(
|
||||||
@@ -62,6 +65,9 @@ class UiSettingsFlow(
|
|||||||
automaticallyProposeAiImprovements,
|
automaticallyProposeAiImprovements,
|
||||||
useTrackedBroadcasts,
|
useTrackedBroadcasts,
|
||||||
bottomBarItems,
|
bottomBarItems,
|
||||||
|
showHomeNewThreadsTab,
|
||||||
|
showHomeConversationsTab,
|
||||||
|
showHomeEverythingTab,
|
||||||
)
|
)
|
||||||
|
|
||||||
// emits at every change in any of the propertyes.
|
// emits at every change in any of the propertyes.
|
||||||
@@ -84,6 +90,9 @@ class UiSettingsFlow(
|
|||||||
flows[12] as BooleanType,
|
flows[12] as BooleanType,
|
||||||
flows[13] as BooleanType,
|
flows[13] as BooleanType,
|
||||||
flows[14] as List<NavBarItem>,
|
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,
|
automaticallyProposeAiImprovements.value,
|
||||||
useTrackedBroadcasts.value,
|
useTrackedBroadcasts.value,
|
||||||
bottomBarItems.value,
|
bottomBarItems.value,
|
||||||
|
showHomeNewThreadsTab.value,
|
||||||
|
showHomeConversationsTab.value,
|
||||||
|
showHomeEverythingTab.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun update(torSettings: UiSettings): Boolean {
|
fun update(torSettings: UiSettings): Boolean {
|
||||||
@@ -169,6 +181,18 @@ class UiSettingsFlow(
|
|||||||
bottomBarItems.tryEmit(torSettings.bottomBarItems)
|
bottomBarItems.tryEmit(torSettings.bottomBarItems)
|
||||||
any = true
|
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
|
return any
|
||||||
}
|
}
|
||||||
@@ -203,6 +227,9 @@ class UiSettingsFlow(
|
|||||||
MutableStateFlow(uiSettings.automaticallyProposeAiImprovements),
|
MutableStateFlow(uiSettings.automaticallyProposeAiImprovements),
|
||||||
MutableStateFlow(uiSettings.useTrackedBroadcasts),
|
MutableStateFlow(uiSettings.useTrackedBroadcasts),
|
||||||
MutableStateFlow(uiSettings.bottomBarItems),
|
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 VIDEO_SCREEN = "VideoFeed"
|
||||||
const val HOME_FOLLOWS = "HomeFollowsFeed"
|
const val HOME_FOLLOWS = "HomeFollowsFeed"
|
||||||
const val HOME_REPLIES = "HomeFollowsRepliesFeed"
|
const val HOME_REPLIES = "HomeFollowsRepliesFeed"
|
||||||
|
const val HOME_EVERYTHING = "HomeFollowsEverythingFeed"
|
||||||
const val MESSAGES_KNOWN = "MessagesKnown"
|
const val MESSAGES_KNOWN = "MessagesKnown"
|
||||||
const val MESSAGES_NEW = "MessagesNew"
|
const val MESSAGES_NEW = "MessagesNew"
|
||||||
const val PROFILE_GALLERY = "ProfileGalleryFeed"
|
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.AllSettingsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.BottomBarSettingsScreen
|
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.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.NIP47SetupScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen
|
||||||
@@ -305,6 +306,7 @@ fun BuildNavigation(
|
|||||||
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.UserSettings> { UserSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.ReactionsSettings> { ReactionsSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.BottomBarSettings> { BottomBarSettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.BottomBarSettings> { BottomBarSettingsScreen(accountViewModel, nav) }
|
||||||
|
composableFromEnd<Route.HomeTabsSettings> { HomeTabsSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.VideoPlayerSettings> { VideoPlayerSettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.VideoPlayerSettings> { VideoPlayerSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.CallSettings> { CallSettingsScreen(accountViewModel, nav) }
|
composableFromEnd<Route.CallSettings> { CallSettingsScreen(accountViewModel, nav) }
|
||||||
composableFromEnd<Route.ImportFollowsSelectUser> { ImportFollowListSelectUserScreen(accountViewModel, nav) }
|
composableFromEnd<Route.ImportFollowsSelectUser> { ImportFollowListSelectUserScreen(accountViewModel, nav) }
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ sealed class Route {
|
|||||||
|
|
||||||
@Serializable object BottomBarSettings : Route()
|
@Serializable object BottomBarSettings : Route()
|
||||||
|
|
||||||
|
@Serializable object HomeTabsSettings : Route()
|
||||||
|
|
||||||
@Serializable object VideoPlayerSettings : Route()
|
@Serializable object VideoPlayerSettings : Route()
|
||||||
|
|
||||||
@Serializable object CallSettings : 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.emojipacks.browse.dal.BrowseEmojiSetsFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.list.dal.FollowPacksFeedFilter
|
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.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.HomeLiveFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.dal.HomeNewThreadFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.dal.LiveStreamsFeedFilter
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.livestreams.dal.LiveStreamsFeedFilter
|
||||||
@@ -72,6 +73,7 @@ class AccountFeedContentStates(
|
|||||||
val homeLive = ChannelFeedContentState(HomeLiveFilter(account), scope)
|
val homeLive = ChannelFeedContentState(HomeLiveFilter(account), scope)
|
||||||
val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope, LocalCache)
|
val homeNewThreads = FeedContentState(HomeNewThreadFeedFilter(account), scope, LocalCache)
|
||||||
val homeReplies = FeedContentState(HomeConversationsFeedFilter(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 dmKnown = FeedContentState(ChatroomListKnownFeedFilter(account), scope, LocalCache)
|
||||||
val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope, LocalCache)
|
val dmNew = FeedContentState(ChatroomListNewFeedFilter(account), scope, LocalCache)
|
||||||
@@ -139,6 +141,7 @@ class AccountFeedContentStates(
|
|||||||
homeLive.updateFeedWith(newNotes)
|
homeLive.updateFeedWith(newNotes)
|
||||||
homeNewThreads.updateFeedWith(newNotes)
|
homeNewThreads.updateFeedWith(newNotes)
|
||||||
homeReplies.updateFeedWith(newNotes)
|
homeReplies.updateFeedWith(newNotes)
|
||||||
|
homeEverything.updateFeedWith(newNotes)
|
||||||
|
|
||||||
dmKnown.updateFeedWith(newNotes)
|
dmKnown.updateFeedWith(newNotes)
|
||||||
dmNew.updateFeedWith(newNotes)
|
dmNew.updateFeedWith(newNotes)
|
||||||
@@ -186,6 +189,7 @@ class AccountFeedContentStates(
|
|||||||
homeLive.deleteFromFeed(newNotes)
|
homeLive.deleteFromFeed(newNotes)
|
||||||
homeNewThreads.deleteFromFeed(newNotes)
|
homeNewThreads.deleteFromFeed(newNotes)
|
||||||
homeReplies.deleteFromFeed(newNotes)
|
homeReplies.deleteFromFeed(newNotes)
|
||||||
|
homeEverything.deleteFromFeed(newNotes)
|
||||||
|
|
||||||
dmKnown.updateFeedWith(newNotes)
|
dmKnown.updateFeedWith(newNotes)
|
||||||
dmNew.updateFeedWith(newNotes)
|
dmNew.updateFeedWith(newNotes)
|
||||||
|
|||||||
+81
-33
@@ -108,6 +108,7 @@ fun HomeScreen(
|
|||||||
liveFeedState = accountViewModel.feedStates.homeLive,
|
liveFeedState = accountViewModel.feedStates.homeLive,
|
||||||
newThreadsFeedState = accountViewModel.feedStates.homeNewThreads,
|
newThreadsFeedState = accountViewModel.feedStates.homeNewThreads,
|
||||||
repliesFeedState = accountViewModel.feedStates.homeReplies,
|
repliesFeedState = accountViewModel.feedStates.homeReplies,
|
||||||
|
everythingFeedState = accountViewModel.feedStates.homeEverything,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nav = nav,
|
nav = nav,
|
||||||
)
|
)
|
||||||
@@ -119,18 +120,20 @@ fun HomeScreen(
|
|||||||
liveFeedState: ChannelFeedContentState,
|
liveFeedState: ChannelFeedContentState,
|
||||||
newThreadsFeedState: FeedContentState,
|
newThreadsFeedState: FeedContentState,
|
||||||
repliesFeedState: FeedContentState,
|
repliesFeedState: FeedContentState,
|
||||||
|
everythingFeedState: FeedContentState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
WatchAccountForHomeScreen(liveFeedState, newThreadsFeedState, repliesFeedState, accountViewModel)
|
WatchAccountForHomeScreen(liveFeedState, newThreadsFeedState, repliesFeedState, everythingFeedState, accountViewModel)
|
||||||
|
|
||||||
WatchLifecycleAndUpdateModel(liveFeedState)
|
WatchLifecycleAndUpdateModel(liveFeedState)
|
||||||
WatchLifecycleAndUpdateModel(newThreadsFeedState)
|
WatchLifecycleAndUpdateModel(newThreadsFeedState)
|
||||||
WatchLifecycleAndUpdateModel(repliesFeedState)
|
WatchLifecycleAndUpdateModel(repliesFeedState)
|
||||||
|
WatchLifecycleAndUpdateModel(everythingFeedState)
|
||||||
|
|
||||||
HomeFilterAssemblerSubscription(accountViewModel)
|
HomeFilterAssemblerSubscription(accountViewModel)
|
||||||
|
|
||||||
AssembleHomeTabs(newThreadsFeedState, repliesFeedState, liveFeedState) { pagerState, tabItems ->
|
AssembleHomeTabs(newThreadsFeedState, repliesFeedState, everythingFeedState, liveFeedState, accountViewModel) { pagerState, tabItems ->
|
||||||
HomePages(pagerState, tabItems, accountViewModel, nav)
|
HomePages(pagerState, tabItems, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,33 +143,74 @@ fun HomeScreen(
|
|||||||
private fun AssembleHomeTabs(
|
private fun AssembleHomeTabs(
|
||||||
newThreadsFeedState: FeedContentState,
|
newThreadsFeedState: FeedContentState,
|
||||||
repliesFeedState: FeedContentState,
|
repliesFeedState: FeedContentState,
|
||||||
|
everythingFeedState: FeedContentState,
|
||||||
liveFeedState: ChannelFeedContentState,
|
liveFeedState: ChannelFeedContentState,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
inner: @Composable (PagerState, ImmutableList<TabItem>) -> Unit,
|
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
|
val tabs by
|
||||||
remember(newThreadsFeedState, repliesFeedState) {
|
remember(newThreadsFeedState, repliesFeedState, everythingFeedState, showNewThreads, showConversations, showEverything) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
listOf(
|
buildList {
|
||||||
TabItem(
|
if (showNewThreads) {
|
||||||
resource = R.string.new_threads,
|
add(
|
||||||
feedState = newThreadsFeedState,
|
TabItem(
|
||||||
routeForLastRead = "HomeFollows",
|
resource = R.string.new_threads,
|
||||||
scrollStateKey = ScrollStateKeys.HOME_FOLLOWS,
|
feedState = newThreadsFeedState,
|
||||||
liveSection = liveFeedState,
|
routeForLastRead = "HomeFollows",
|
||||||
),
|
scrollStateKey = ScrollStateKeys.HOME_FOLLOWS,
|
||||||
TabItem(
|
liveSection = liveFeedState,
|
||||||
resource = R.string.conversations,
|
),
|
||||||
feedState = repliesFeedState,
|
)
|
||||||
routeForLastRead = "HomeFollowsReplies",
|
}
|
||||||
scrollStateKey = ScrollStateKeys.HOME_REPLIES,
|
if (showConversations) {
|
||||||
liveSection = liveFeedState,
|
add(
|
||||||
),
|
TabItem(
|
||||||
).toImmutableList(),
|
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)
|
inner(pagerState, tabs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,19 +226,21 @@ private fun HomePages(
|
|||||||
topBar = {
|
topBar = {
|
||||||
Column {
|
Column {
|
||||||
HomeTopBar(accountViewModel, nav)
|
HomeTopBar(accountViewModel, nav)
|
||||||
SecondaryTabRow(
|
if (tabs.size > 1) {
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
SecondaryTabRow(
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
modifier = TabRowHeight,
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
selectedTabIndex = pagerState.currentPage,
|
modifier = TabRowHeight,
|
||||||
) {
|
selectedTabIndex = pagerState.currentPage,
|
||||||
val coroutineScope = rememberCoroutineScope()
|
) {
|
||||||
tabs.forEachIndexed { index, tab ->
|
val coroutineScope = rememberCoroutineScope()
|
||||||
Tab(
|
tabs.forEachIndexed { index, tab ->
|
||||||
selected = pagerState.currentPage == index,
|
Tab(
|
||||||
text = { Text(text = stringRes(tab.resource)) },
|
selected = pagerState.currentPage == index,
|
||||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
text = { Text(text = stringRes(tab.resource)) },
|
||||||
)
|
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(index) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -506,6 +552,7 @@ fun WatchAccountForHomeScreen(
|
|||||||
liveFeedState: ChannelFeedContentState,
|
liveFeedState: ChannelFeedContentState,
|
||||||
newThreadsFeedState: FeedContentState,
|
newThreadsFeedState: FeedContentState,
|
||||||
repliesFeedState: FeedContentState,
|
repliesFeedState: FeedContentState,
|
||||||
|
everythingFeedState: FeedContentState,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
) {
|
) {
|
||||||
val homeFollowList by accountViewModel.account.liveHomeFollowLists.collectAsStateWithLifecycle()
|
val homeFollowList by accountViewModel.account.liveHomeFollowLists.collectAsStateWithLifecycle()
|
||||||
@@ -513,6 +560,7 @@ fun WatchAccountForHomeScreen(
|
|||||||
LaunchedEffect(accountViewModel, homeFollowList) {
|
LaunchedEffect(accountViewModel, homeFollowList) {
|
||||||
newThreadsFeedState.checkKeysInvalidateDataAndSendToTop()
|
newThreadsFeedState.checkKeysInvalidateDataAndSendToTop()
|
||||||
repliesFeedState.checkKeysInvalidateDataAndSendToTop()
|
repliesFeedState.checkKeysInvalidateDataAndSendToTop()
|
||||||
|
everythingFeedState.checkKeysInvalidateDataAndSendToTop()
|
||||||
liveFeedState.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,
|
tint = tint,
|
||||||
onClick = { nav.nav(Route.BottomBarSettings) },
|
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)
|
HorizontalDivider(thickness = 4.dp)
|
||||||
SettingsSectionHeader(R.string.danger_zone)
|
SettingsSectionHeader(R.string.danger_zone)
|
||||||
accountViewModel.account.settings.keyPair.privKey?.let {
|
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_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_available">Available</string>
|
||||||
<string name="bottom_bar_settings_reorder">Reorder</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">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_description">Configure which reaction buttons are shown, their order, and whether to display counters.</string>
|
||||||
<string name="reactions_settings_enabled">Enabled</string>
|
<string name="reactions_settings_enabled">Enabled</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user