diff --git a/core/navigation/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/navigation/NavigationCoordinator.kt b/core/navigation/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/navigation/NavigationCoordinator.kt index b6473962a..393e704da 100644 --- a/core/navigation/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/navigation/NavigationCoordinator.kt +++ b/core/navigation/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/core/navigation/NavigationCoordinator.kt @@ -19,6 +19,10 @@ sealed interface TabNavigationSection { data object Profile : TabNavigationSection data object Inbox : TabNavigationSection + + data object Settings : TabNavigationSection + + data object Bookmarks : TabNavigationSection } sealed interface ComposeEvent { diff --git a/feature/settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/SettingsTab.kt b/feature/settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/SettingsTab.kt new file mode 100644 index 000000000..65fc72a4c --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/feature/settings/SettingsTab.kt @@ -0,0 +1,31 @@ +package com.github.diegoberaldin.raccoonforlemmy.feature.settings + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions +import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings +import com.github.diegoberaldin.raccoonforlemmy.feature.settings.main.SettingsScreen + +object SettingsTab : Tab { + override val options: TabOptions + @Composable + get() { + val icon = rememberVectorPainter(Icons.Default.Settings) + val title = LocalStrings.current.navigationSettings + + return TabOptions( + index = 4u, + title = title, + icon = icon, + ) + } + + @Composable + override fun Content() { + Navigator(SettingsScreen()) + } +} diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 42ccf3171..6c1c51a8e 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -33,8 +33,9 @@ kotlin { dependencies { implementation(compose.runtime) implementation(compose.foundation) - implementation(compose.material3) implementation(compose.material) + implementation(compose.material3) + implementation(compose.materialIconsExtended) @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) implementation(compose.components.resources) diff --git a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/MainMviModel.kt b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/MainMviModel.kt index 80c1b7f4b..6dc42b977 100644 --- a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/MainMviModel.kt +++ b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/MainMviModel.kt @@ -2,6 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy import cafe.adriel.voyager.core.model.ScreenModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel +import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection interface MainMviModel : MviModel, @@ -18,6 +19,13 @@ interface MainMviModel : val bottomBarOffsetHeightPx: Float = 0f, val customProfileUrl: String? = null, val isLogged: Boolean = false, + val bottomBarSections: List = + listOf( + TabNavigationSection.Home, + TabNavigationSection.Explore, + TabNavigationSection.Inbox, + TabNavigationSection.Profile, + ), ) sealed interface Effect { diff --git a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/MainScreen.kt b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/MainScreen.kt index 740cce5ca..f811fa5a3 100644 --- a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/MainScreen.kt +++ b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/MainScreen.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.toSize import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.navigator.tab.CurrentTab +import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabNavigator import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings @@ -42,13 +43,10 @@ import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getDrawerCoor import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter -import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback +import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs import com.github.diegoberaldin.raccoonforlemmy.feature.home.ui.HomeTab -import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.ui.InboxTab -import com.github.diegoberaldin.raccoonforlemmy.feature.profile.ui.ProfileTab -import com.github.diegoberaldin.raccoonforlemmy.feature.search.ui.ExploreTab import com.github.diegoberaldin.raccoonforlemmy.feature.settings.main.SettingsScreen -import com.github.diegoberaldin.raccoonforlemmy.ui.navigation.TabNavigationItem +import com.github.diegoberaldin.raccoonforlemmy.navigation.TabNavigationItem import com.github.diegoberaldin.raccoonforlemmy.unit.manageaccounts.ManageAccountsScreen import kotlinx.coroutines.delay import kotlinx.coroutines.flow.drop @@ -225,6 +223,37 @@ internal object MainScreen : Screen { uiFontSizeWorkaround = true }.launchIn(this) } + + fun handleOnLongPress( + tab: Tab, + section: TabNavigationSection, + ) { + when (section) { + TabNavigationSection.Explore -> { + tabNavigator.current = tab + navigationCoordinator.setCurrentSection(TabNavigationSection.Explore) + scope.launch { + notificationCenter.send(NotificationCenterEvent.OpenSearchInExplore) + } + } + + TabNavigationSection.Inbox -> { + if (uiState.isLogged) { + model.reduce(MainMviModel.Intent.ReadAllInbox) + } + } + + TabNavigationSection.Profile -> { + if (uiState.isLogged) { + val screen = ManageAccountsScreen() + navigationCoordinator.showBottomSheet(screen) + } + } + + else -> Unit + } + } + if (uiFontSizeWorkaround) { NavigationBar( modifier = @@ -248,45 +277,27 @@ internal object MainScreen : Screen { ), tonalElevation = 0.dp, ) { - TabNavigationItem( - tab = HomeTab, - withText = titleVisible, - ) - TabNavigationItem( - tab = ExploreTab, - withText = titleVisible, - onLongPress = - rememberCallback { - tabNavigator.current = ExploreTab - navigationCoordinator.setCurrentSection(TabNavigationSection.Explore) - - scope.launch { - notificationCenter.send(NotificationCenterEvent.OpenSearchInExplore) - } - }, - ) - TabNavigationItem( - tab = InboxTab, - withText = titleVisible, - onLongPress = - rememberCallback(model) { - if (uiState.isLogged) { - model.reduce(MainMviModel.Intent.ReadAllInbox) - } - }, - ) - TabNavigationItem( - tab = ProfileTab, - withText = titleVisible, - customIconUrl = uiState.customProfileUrl, - onLongPress = - rememberCallback { - if (uiState.isLogged) { - val screen = ManageAccountsScreen() - navigationCoordinator.showBottomSheet(screen) - } - }, - ) + for (section in uiState.bottomBarSections) { + TabNavigationItem( + section = section, + withText = titleVisible, + customIconUrl = + if (section == TabNavigationSection.Profile) { + uiState.customProfileUrl + } else { + null + }, + onClick = + rememberCallbackArgs { tab -> + tabNavigator.current = tab + navigationCoordinator.setCurrentSection(section) + }, + onLongPress = + rememberCallbackArgs { tab -> + handleOnLongPress(tab, section) + }, + ) + } } } } diff --git a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/navigation/BookmarksTab.kt b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/navigation/BookmarksTab.kt new file mode 100644 index 000000000..49759592c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/navigation/BookmarksTab.kt @@ -0,0 +1,37 @@ +package com.github.diegoberaldin.raccoonforlemmy.navigation + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bookmark +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabOptions +import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings +import com.github.diegoberaldin.raccoonforlemmy.unit.filteredcontents.FilteredContentsScreen +import com.github.diegoberaldin.raccoonforlemmy.unit.filteredcontents.FilteredContentsType +import com.github.diegoberaldin.raccoonforlemmy.unit.filteredcontents.toInt + +object BookmarksTab : Tab { + override val options: TabOptions + @Composable + get() { + val icon = rememberVectorPainter(Icons.Default.Bookmark) + val title = LocalStrings.current.navigationDrawerTitleBookmarks + + return TabOptions( + index = 5u, + title = title, + icon = icon, + ) + } + + @Composable + override fun Content() { + Navigator( + FilteredContentsScreen( + type = FilteredContentsType.Bookmarks.toInt(), + ), + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/ui/navigation/TabNavigationItem.kt b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/navigation/TabNavigationItem.kt similarity index 84% rename from shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/ui/navigation/TabNavigationItem.kt rename to shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/navigation/TabNavigationItem.kt index ea2156cd7..b61fe590b 100644 --- a/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/ui/navigation/TabNavigationItem.kt +++ b/shared/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/navigation/TabNavigationItem.kt @@ -1,4 +1,4 @@ -package com.github.diegoberaldin.raccoonforlemmy.ui.navigation +package com.github.diegoberaldin.raccoonforlemmy.navigation import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -24,7 +24,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.style.TextOverflow -import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing @@ -32,39 +31,28 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomI import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings import com.github.diegoberaldin.raccoonforlemmy.core.navigation.TabNavigationSection import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator +import com.github.diegoberaldin.raccoonforlemmy.feature.home.ui.HomeTab import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.ui.InboxTab import com.github.diegoberaldin.raccoonforlemmy.feature.profile.ui.ProfileTab import com.github.diegoberaldin.raccoonforlemmy.feature.search.ui.ExploreTab +import com.github.diegoberaldin.raccoonforlemmy.feature.settings.SettingsTab @Composable internal fun RowScope.TabNavigationItem( - tab: Tab, + section: TabNavigationSection, withText: Boolean = true, customIconUrl: String? = null, - onLongPress: (() -> Unit)? = null, + onClick: ((Tab) -> Unit)? = null, + onLongPress: ((Tab) -> Unit)? = null, ) { - val tabNavigator = LocalTabNavigator.current val navigationCoordinator = remember { getNavigationCoordinator() } val unread by navigationCoordinator.inboxUnread.collectAsState() - val color = - if (tabNavigator.current == tab) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.outline - } - + val currentSection by navigationCoordinator.currentSection.collectAsState() val interactionSource = remember { MutableInteractionSource() } + val tab = section.toTab() fun handleClick() { - tabNavigator.current = tab - val section = - when (tab) { - ExploreTab -> TabNavigationSection.Explore - ProfileTab -> TabNavigationSection.Profile - InboxTab -> TabNavigationSection.Inbox - else -> TabNavigationSection.Home - } - navigationCoordinator.setCurrentSection(section) + onClick?.invoke(tab) } val pointerInputModifier = @@ -80,14 +68,14 @@ internal fun RowScope.TabNavigationItem( handleClick() }, onLongPress = { - onLongPress?.invoke() + onLongPress?.invoke(tab) }, ) } NavigationBarItem( onClick = ::handleClick, - selected = tabNavigator.current == tab, + selected = section == currentSection, interactionSource = interactionSource, icon = { val content = @Composable { @@ -107,7 +95,6 @@ internal fun RowScope.TabNavigationItem( modifier = pointerInputModifier, painter = tab.options.icon ?: rememberVectorPainter(Icons.Default.Home), contentDescription = null, - tint = color, ) } } @@ -141,7 +128,6 @@ internal fun RowScope.TabNavigationItem( Text( modifier = Modifier, text = tab.options.title, - color = color, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -149,3 +135,13 @@ internal fun RowScope.TabNavigationItem( }, ) } + +private fun TabNavigationSection.toTab(): Tab = + when (this) { + TabNavigationSection.Explore -> ExploreTab + TabNavigationSection.Profile -> ProfileTab + TabNavigationSection.Inbox -> InboxTab + TabNavigationSection.Settings -> SettingsTab + TabNavigationSection.Bookmarks -> BookmarksTab + else -> HomeTab + } diff --git a/unit/filteredcontents/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/filteredcontents/FilteredContentsScreen.kt b/unit/filteredcontents/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/filteredcontents/FilteredContentsScreen.kt index 048e8ced5..ab94d22ff 100644 --- a/unit/filteredcontents/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/filteredcontents/FilteredContentsScreen.kt +++ b/unit/filteredcontents/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/filteredcontents/FilteredContentsScreen.kt @@ -5,10 +5,10 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.ArrowCircleDown import androidx.compose.material.icons.filled.ArrowCircleUp import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.Menu import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -49,6 +50,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -60,6 +62,7 @@ import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.getScreenModel import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository +import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Dimensions import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.FloatingActionButtonMenu import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.FloatingActionButtonMenuItem @@ -74,12 +77,14 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.PostCardPl import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.di.getFabNestedScrollConnection import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.LikedTypeSheet import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings +import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getDrawerCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.ActionOnSwipe import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs +import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalPixel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.readableHandle @@ -97,6 +102,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.koin.core.parameter.parametersOf +import kotlin.math.roundToInt class FilteredContentsScreen( private val type: Int, @@ -111,6 +117,7 @@ class FilteredContentsScreen( val fabNestedScrollConnection = remember { getFabNestedScrollConnection() } val isFabVisible by fabNestedScrollConnection.isFabVisible.collectAsState() val navigationCoordinator = remember { getNavigationCoordinator() } + val drawerCoordinator = remember { getDrawerCoordinator() } val settingsRepository = remember { getSettingsRepository() } val settings by settingsRepository.currentSettings.collectAsState() val lazyListState = rememberLazyListState() @@ -137,43 +144,74 @@ class FilteredContentsScreen( val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(model) { - model.effects.onEach { effect -> - when (effect) { - FilteredContentsMviModel.Effect.BackToTop -> { - runCatching { - lazyListState.scrollToItem(0) - topAppBarState.heightOffset = 0f - topAppBarState.contentOffset = 0f + model.effects + .onEach { effect -> + when (effect) { + FilteredContentsMviModel.Effect.BackToTop -> { + runCatching { + lazyListState.scrollToItem(0) + topAppBarState.heightOffset = 0f + topAppBarState.contentOffset = 0f + } } } - } - }.launchIn(this) + }.launchIn(this) } LaunchedEffect(navigationCoordinator) { - navigationCoordinator.globalMessage.onEach { message -> - snackbarHostState.showSnackbar( - message = message, - ) - }.launchIn(this) + navigationCoordinator.globalMessage + .onEach { message -> + snackbarHostState.showSnackbar( + message = message, + ) + }.launchIn(this) } Scaffold( modifier = Modifier.background(MaterialTheme.colorScheme.background), topBar = { + val maxTopInset = Dimensions.maxTopBarInset.toLocalPixel() + var topInset by remember { mutableStateOf(maxTopInset) } + snapshotFlow { topAppBarState.collapsedFraction } + .onEach { + topInset = maxTopInset * (1 - it) + }.launchIn(scope) TopAppBar( + windowInsets = + if (settings.edgeToEdge) { + WindowInsets(0, topInset.roundToInt(), 0, 0) + } else { + TopAppBarDefaults.windowInsets + }, scrollBehavior = scrollBehavior, navigationIcon = { - Image( - modifier = - Modifier.onClick( - onClick = { - navigationCoordinator.popScreen() - }, - ), - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), - ) + if (navigationCoordinator.canPop.value) { + Image( + modifier = + Modifier + .onClick( + onClick = { + navigationCoordinator.popScreen() + }, + ), + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), + ) + } else { + Image( + modifier = + Modifier.onClick( + onClick = { + scope.launch { + drawerCoordinator.toggleDrawer() + } + }, + ), + imageVector = Icons.Default.Menu, + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), + ) + } }, title = { Column(modifier = Modifier.padding(horizontal = Spacing.s)) { @@ -252,742 +290,742 @@ class FilteredContentsScreen( } }, ) { padding -> - Column( + Box( modifier = Modifier .padding( top = padding.calculateTopPadding(), - ) - .then( + ).then( if (settings.hideNavigationBarWhileScrolling) { Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) } else { Modifier }, - ), - verticalArrangement = Arrangement.spacedBy(Spacing.s), + ).nestedScroll(fabNestedScrollConnection) + .pullRefresh(pullRefreshState), ) { - if (!uiState.isPostOnly) { - SectionSelector( - titles = - listOf( - LocalStrings.current.profileSectionPosts, - LocalStrings.current.profileSectionComments, - ), - currentSection = - when (uiState.section) { - FilteredContentsSection.Comments -> 1 - else -> 0 - }, - onSectionSelected = { - val section = - when (it) { - 1 -> FilteredContentsSection.Comments - else -> FilteredContentsSection.Posts - } - model.reduce(FilteredContentsMviModel.Intent.ChangeSection(section)) - }, - ) - } - - Box( - modifier = - Modifier - .fillMaxWidth() - .then( - if (settings.hideNavigationBarWhileScrolling) { - Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) - } else { - Modifier + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = lazyListState, + ) { + item { + if (!uiState.isPostOnly) { + SectionSelector( + titles = + listOf( + LocalStrings.current.profileSectionPosts, + LocalStrings.current.profileSectionComments, + ), + currentSection = + when (uiState.section) { + FilteredContentsSection.Comments -> 1 + else -> 0 + }, + onSectionSelected = { + val section = + when (it) { + 1 -> FilteredContentsSection.Comments + else -> FilteredContentsSection.Posts + } + model.reduce( + FilteredContentsMviModel.Intent.ChangeSection( + section, + ), + ) }, ) - .nestedScroll(fabNestedScrollConnection) - .pullRefresh(pullRefreshState), - ) { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyListState, - ) { - if (uiState.section == FilteredContentsSection.Posts) { - if (uiState.posts.isEmpty() && uiState.loading && uiState.initial) { - items(5) { - PostCardPlaceholder( - postLayout = uiState.postLayout, - ) - if (uiState.postLayout != PostLayout.Card) { - HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem)) - } else { - Spacer(modifier = Modifier.height(Spacing.interItem)) - } - } - } - if (uiState.posts.isEmpty() && !uiState.initial && !uiState.loading) { - item { - Text( - modifier = Modifier.fillMaxWidth().padding(top = Spacing.xs), - textAlign = TextAlign.Center, - text = LocalStrings.current.messageEmptyList, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground, - ) - } - } - items( - items = uiState.posts, - key = { - it.id.toString() + (it.updateDate ?: it.publishDate) - }, - ) { post -> - - @Composable - fun List.toSwipeActions(): List = - mapNotNull { - when (it) { - ActionOnSwipe.UpVote -> - SwipeAction( - swipeContent = { - Icon( - imageVector = Icons.Default.ArrowCircleUp, - contentDescription = null, - tint = Color.White, - ) - }, - backgroundColor = upVoteColor ?: defaultUpvoteColor, - onTriggered = - rememberCallback { - model.reduce( - FilteredContentsMviModel.Intent.UpVotePost( - post.id, - ), - ) - }, - ) - - ActionOnSwipe.DownVote -> - if (!uiState.downVoteEnabled) { - null - } else { - SwipeAction( - swipeContent = { - Icon( - imageVector = Icons.Default.ArrowCircleDown, - contentDescription = null, - tint = Color.White, - ) - }, - backgroundColor = - downVoteColor - ?: defaultDownVoteColor, - onTriggered = - rememberCallback { - model.reduce( - FilteredContentsMviModel.Intent.DownVotePost( - post.id, - ), - ) - }, - ) - } - - ActionOnSwipe.Reply -> - SwipeAction( - swipeContent = { - Icon( - imageVector = Icons.AutoMirrored.Default.Reply, - contentDescription = null, - tint = Color.White, - ) - }, - backgroundColor = replyColor ?: defaultReplyColor, - onTriggered = - rememberCallback { - detailOpener.openReply(originalPost = post) - }, - ) - - ActionOnSwipe.Save -> - SwipeAction( - swipeContent = { - Icon( - imageVector = Icons.Default.Bookmark, - contentDescription = null, - tint = Color.White, - ) - }, - backgroundColor = saveColor ?: defaultSaveColor, - onTriggered = - rememberCallback { - model.reduce( - FilteredContentsMviModel.Intent.SavePost( - post.id, - ), - ) - }, - ) - - else -> null - } - } - SwipeActionCard( - modifier = Modifier.fillMaxWidth(), - enabled = uiState.swipeActionsEnabled, - onGestureBegin = - rememberCallback(model) { - model.reduce(FilteredContentsMviModel.Intent.HapticIndication) - }, - swipeToStartActions = uiState.actionsOnSwipeToStartPosts.toSwipeActions(), - swipeToEndActions = uiState.actionsOnSwipeToEndPosts.toSwipeActions(), - content = { - PostCard( - post = post, - postLayout = uiState.postLayout, - limitBodyHeight = true, - fullHeightImage = uiState.fullHeightImages, - fullWidthImage = uiState.fullWidthImages, - voteFormat = uiState.voteFormat, - autoLoadImages = uiState.autoLoadImages, - preferNicknames = uiState.preferNicknames, - fadeRead = uiState.fadeReadPosts, - showUnreadComments = uiState.showUnreadComments, - downVoteEnabled = uiState.downVoteEnabled, - onClick = - rememberCallback(model) { - model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) - detailOpener.openPostDetail(post) - }, - onOpenCommunity = - rememberCallbackArgs { community, instance -> - detailOpener.openCommunityDetail( - community, - instance, - ) - }, - onOpenCreator = - rememberCallbackArgs { user, instance -> - detailOpener.openUserDetail(user, instance) - }, - onOpenPost = - rememberCallbackArgs { p, instance -> - detailOpener.openPostDetail(p, instance) - }, - onOpenWeb = - rememberCallbackArgs { url -> - navigationCoordinator.pushScreen( - WebViewScreen(url), - ) - }, - onUpVote = - rememberCallback(model) { - model.reduce( - FilteredContentsMviModel.Intent.UpVotePost(post.id), - ) - }, - onDownVote = - rememberCallback(model) { - model.reduce( - FilteredContentsMviModel.Intent.DownVotePost( - post.id, - ), - ) - }, - onSave = - rememberCallback(model) { - model.reduce( - FilteredContentsMviModel.Intent.SavePost(post.id), - ) - }, - onReply = - rememberCallback(model) { - model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) - detailOpener.openPostDetail(post) - }, - onOpenImage = - rememberCallbackArgs(model, post) { url -> - navigationCoordinator.pushScreen( - ZoomableImageScreen( - url = url, - source = post.community?.readableHandle.orEmpty(), - ), - ) - }, - options = - buildList { - this += - Option( - OptionId.SeeRaw, - LocalStrings.current.postActionSeeRaw, - ) - if (uiState.contentsType == FilteredContentsType.Moderated) { - this += - Option( - OptionId.FeaturePost, - if (post.featuredCommunity) { - LocalStrings.current.modActionUnmarkAsFeatured - } else { - LocalStrings.current.modActionMarkAsFeatured - }, - ) - this += - Option( - OptionId.LockPost, - if (post.locked) { - LocalStrings.current.modActionUnlock - } else { - LocalStrings.current.modActionLock - }, - ) - this += - Option( - OptionId.BanUser, - if (post.creator?.banned == true) { - LocalStrings.current.modActionAllow - } else { - LocalStrings.current.modActionBan - }, - ) - this += - Option( - OptionId.Remove, - LocalStrings.current.modActionRemove, - ) - } - if (uiState.isAdmin && uiState.contentsType == FilteredContentsType.Moderated) { - this += - Option( - OptionId.Purge, - LocalStrings.current.adminActionPurge, - ) - post.creator?.also { creator -> - this += - Option( - OptionId.PurgeCreator, - buildString { - append(LocalStrings.current.adminActionPurge) - append(" ") - append(creator.readableName(uiState.preferNicknames)) - }, - ) - } - this += - Option( - OptionId.AdminFeaturePost, - if (post.featuredLocal) { - LocalStrings.current.adminActionUnmarkAsFeatured - } else { - LocalStrings.current.adminActionMarkAsFeatured - }, - ) - } - }, - onOptionSelected = - rememberCallbackArgs(model) { optionId -> - when (optionId) { - OptionId.SeeRaw -> { - rawContent = post - } - - OptionId.FeaturePost -> - model.reduce( - FilteredContentsMviModel.Intent.ModFeaturePost(post.id), - ) - - OptionId.AdminFeaturePost -> - model.reduce( - FilteredContentsMviModel.Intent.AdminFeaturePost(post.id), - ) - - OptionId.LockPost -> - model.reduce( - FilteredContentsMviModel.Intent.ModLockPost(post.id), - ) - - OptionId.Remove -> { - val screen = - ModerateWithReasonScreen( - actionId = ModerateWithReasonAction.RemovePost.toInt(), - contentId = post.id, - ) - navigationCoordinator.pushScreen(screen) - } - - OptionId.BanUser -> { - post.creator?.id?.also { userId -> - post.community?.id?.also { communityId -> - val screen = - BanUserScreen( - userId = userId, - communityId = communityId, - newValue = post.creator?.banned != true, - postId = post.id, - ) - navigationCoordinator.pushScreen(screen) - } - } - } - - OptionId.Purge -> { - val screen = - ModerateWithReasonScreen( - actionId = ModerateWithReasonAction.PurgePost.toInt(), - contentId = post.id, - ) - navigationCoordinator.pushScreen(screen) - } - - OptionId.PurgeCreator -> { - post.creator?.id?.also { userId -> - val screen = - ModerateWithReasonScreen( - actionId = ModerateWithReasonAction.PurgeUser.toInt(), - contentId = userId, - ) - navigationCoordinator.pushScreen(screen) - } - } - - else -> Unit - } - }, - ) - }, - ) - - if (uiState.postLayout != PostLayout.Card) { - HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem)) - } else { - Spacer(modifier = Modifier.height(Spacing.interItem)) - } - } - } else { - if (uiState.comments.isEmpty() && uiState.loading && uiState.initial) { - items(5) { - ModdedCommentPlaceholder(postLayout = uiState.postLayout) - if (uiState.postLayout != PostLayout.Card) { - HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem)) - } else { - Spacer(modifier = Modifier.height(Spacing.interItem)) - } - } - } - if (uiState.comments.isEmpty() && !uiState.initial && !uiState.loading) { - item { - Text( - modifier = Modifier.fillMaxWidth().padding(top = Spacing.xs), - textAlign = TextAlign.Center, - text = LocalStrings.current.messageEmptyList, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onBackground, - ) - } - } - items( - uiState.comments, - { it.id.toString() + (it.updateDate ?: it.publishDate) }, - ) { comment -> - - @Composable - fun List.toSwipeActions(): List = - mapNotNull { - when (it) { - ActionOnSwipe.UpVote -> - SwipeAction( - swipeContent = { - Icon( - imageVector = Icons.Default.ArrowCircleUp, - contentDescription = null, - tint = Color.White, - ) - }, - backgroundColor = upVoteColor ?: defaultUpvoteColor, - onTriggered = - rememberCallback { - model.reduce( - FilteredContentsMviModel.Intent.UpVoteComment( - comment.id, - ), - ) - }, - ) - - ActionOnSwipe.DownVote -> - if (!uiState.downVoteEnabled) { - null - } else { - SwipeAction( - swipeContent = { - Icon( - imageVector = Icons.Default.ArrowCircleDown, - contentDescription = null, - tint = Color.White, - ) - }, - backgroundColor = - downVoteColor - ?: defaultDownVoteColor, - onTriggered = - rememberCallback { - model.reduce( - FilteredContentsMviModel.Intent.DownVoteComment( - comment.id, - ), - ) - }, - ) - } - - ActionOnSwipe.Reply -> - SwipeAction( - swipeContent = { - Icon( - imageVector = Icons.AutoMirrored.Default.Reply, - contentDescription = null, - tint = Color.White, - ) - }, - backgroundColor = replyColor ?: defaultReplyColor, - onTriggered = - rememberCallback { - detailOpener.openReply( - originalPost = PostModel(comment.postId), - originalComment = comment, - ) - }, - ) - - ActionOnSwipe.Save -> - SwipeAction( - swipeContent = { - Icon( - imageVector = Icons.Default.Bookmark, - contentDescription = null, - tint = Color.White, - ) - }, - backgroundColor = saveColor ?: defaultSaveColor, - onTriggered = - rememberCallback { - model.reduce( - FilteredContentsMviModel.Intent.SaveComment( - comment.id, - ), - ) - }, - ) - - else -> null - } - } - - SwipeActionCard( - modifier = Modifier.fillMaxWidth(), - enabled = uiState.swipeActionsEnabled, - onGestureBegin = - rememberCallback(model) { - model.reduce(FilteredContentsMviModel.Intent.HapticIndication) - }, - swipeToStartActions = uiState.actionsOnSwipeToStartComments.toSwipeActions(), - swipeToEndActions = uiState.actionsOnSwipeToEndComments.toSwipeActions(), - content = { - ModdedCommentCard( - comment = comment, - postLayout = uiState.postLayout, - voteFormat = uiState.voteFormat, - autoLoadImages = uiState.autoLoadImages, - preferNicknames = uiState.preferNicknames, - downVoteEnabled = uiState.downVoteEnabled, - onOpenUser = - rememberCallbackArgs { user, instance -> - detailOpener.openUserDetail(user, instance) - }, - onOpen = - rememberCallback { - model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) - detailOpener.openPostDetail( - post = PostModel(id = comment.postId), - highlightCommentId = comment.id, - isMod = true, - ) - }, - onUpVote = - rememberCallback(model) { - model.reduce( - FilteredContentsMviModel.Intent.UpVoteComment(comment.id), - ) - }, - onDownVote = - rememberCallback(model) { - model.reduce( - FilteredContentsMviModel.Intent.DownVoteComment(comment.id), - ) - }, - onSave = - rememberCallback(model) { - model.reduce( - FilteredContentsMviModel.Intent.SaveComment(comment.id), - ) - }, - onReply = - rememberCallback { - detailOpener.openReply( - originalPost = PostModel(id = comment.postId), - originalComment = comment, - ) - }, - options = - buildList { - this += - Option( - OptionId.SeeRaw, - LocalStrings.current.postActionSeeRaw, - ) - if (uiState.contentsType == FilteredContentsType.Moderated) { - this += - Option( - OptionId.DistinguishComment, - if (comment.distinguished) { - LocalStrings.current.modActionUnmarkAsDistinguished - } else { - LocalStrings.current.modActionMarkAsDistinguished - }, - ) - this += - Option( - OptionId.BanUser, - if (comment.creator?.banned == true) { - LocalStrings.current.modActionAllow - } else { - LocalStrings.current.modActionBan - }, - ) - this += - Option( - OptionId.Remove, - LocalStrings.current.modActionRemove, - ) - } - if (uiState.isAdmin && uiState.contentsType == FilteredContentsType.Moderated) { - this += - Option( - OptionId.Purge, - LocalStrings.current.adminActionPurge, - ) - comment.creator?.also { creator -> - this += - Option( - OptionId.PurgeCreator, - buildString { - append(LocalStrings.current.adminActionPurge) - append(" ") - append(creator.readableName(uiState.preferNicknames)) - }, - ) - } - } - }, - onOptionSelected = - rememberCallbackArgs { optionId -> - when (optionId) { - OptionId.Remove -> { - val screen = - ModerateWithReasonScreen( - actionId = ModerateWithReasonAction.RemoveComment.toInt(), - contentId = comment.id, - ) - navigationCoordinator.pushScreen(screen) - } - - OptionId.SeeRaw -> { - rawContent = comment - } - - OptionId.DistinguishComment -> - model.reduce( - FilteredContentsMviModel.Intent.ModDistinguishComment( - comment.id, - ), - ) - - OptionId.BanUser -> { - comment.creator?.id?.also { userId -> - comment.community?.id?.also { communityId -> - val screen = - BanUserScreen( - userId = userId, - communityId = communityId, - newValue = comment.creator?.banned != true, - commentId = comment.id, - ) - navigationCoordinator.pushScreen( - screen, - ) - } - } - } - - OptionId.Purge -> { - val screen = - ModerateWithReasonScreen( - actionId = ModerateWithReasonAction.PurgeComment.toInt(), - contentId = comment.id, - ) - navigationCoordinator.pushScreen(screen) - } - - OptionId.PurgeCreator -> { - comment.creator?.id?.also { userId -> - val screen = - ModerateWithReasonScreen( - actionId = ModerateWithReasonAction.PurgeUser.toInt(), - contentId = userId, - ) - navigationCoordinator.pushScreen(screen) - } - } - - else -> Unit - } - }, - ) - }, - ) - - if (uiState.postLayout != PostLayout.Card) { - HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem)) - } else { - Spacer(modifier = Modifier.height(Spacing.interItem)) - } - } - } - - item { - if (!uiState.initial && !uiState.loading && !uiState.refreshing && uiState.canFetchMore) { - model.reduce(FilteredContentsMviModel.Intent.LoadNextPage) - } - if (uiState.loading && !uiState.refreshing) { - Box( - modifier = Modifier.fillMaxWidth().padding(Spacing.xs), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator( - modifier = Modifier.size(25.dp), - color = MaterialTheme.colorScheme.primary, - ) - } - } - } - item { - Spacer(modifier = Modifier.height(Spacing.xxxl)) } } - PullRefreshIndicator( - refreshing = uiState.refreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.onBackground, - ) + if (uiState.section == FilteredContentsSection.Posts) { + if (uiState.posts.isEmpty() && uiState.loading && uiState.initial) { + items(5) { + PostCardPlaceholder( + postLayout = uiState.postLayout, + ) + if (uiState.postLayout != PostLayout.Card) { + HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem)) + } else { + Spacer(modifier = Modifier.height(Spacing.interItem)) + } + } + } + if (uiState.posts.isEmpty() && !uiState.initial && !uiState.loading) { + item { + Text( + modifier = Modifier.fillMaxWidth().padding(top = Spacing.xs), + textAlign = TextAlign.Center, + text = LocalStrings.current.messageEmptyList, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + } + } + items( + items = uiState.posts, + key = { + it.id.toString() + (it.updateDate ?: it.publishDate) + }, + ) { post -> + + @Composable + fun List.toSwipeActions(): List = + mapNotNull { + when (it) { + ActionOnSwipe.UpVote -> + SwipeAction( + swipeContent = { + Icon( + imageVector = Icons.Default.ArrowCircleUp, + contentDescription = null, + tint = Color.White, + ) + }, + backgroundColor = upVoteColor ?: defaultUpvoteColor, + onTriggered = + rememberCallback { + model.reduce( + FilteredContentsMviModel.Intent.UpVotePost( + post.id, + ), + ) + }, + ) + + ActionOnSwipe.DownVote -> + if (!uiState.downVoteEnabled) { + null + } else { + SwipeAction( + swipeContent = { + Icon( + imageVector = Icons.Default.ArrowCircleDown, + contentDescription = null, + tint = Color.White, + ) + }, + backgroundColor = + downVoteColor + ?: defaultDownVoteColor, + onTriggered = + rememberCallback { + model.reduce( + FilteredContentsMviModel.Intent.DownVotePost( + post.id, + ), + ) + }, + ) + } + + ActionOnSwipe.Reply -> + SwipeAction( + swipeContent = { + Icon( + imageVector = Icons.AutoMirrored.Default.Reply, + contentDescription = null, + tint = Color.White, + ) + }, + backgroundColor = replyColor ?: defaultReplyColor, + onTriggered = + rememberCallback { + detailOpener.openReply(originalPost = post) + }, + ) + + ActionOnSwipe.Save -> + SwipeAction( + swipeContent = { + Icon( + imageVector = Icons.Default.Bookmark, + contentDescription = null, + tint = Color.White, + ) + }, + backgroundColor = saveColor ?: defaultSaveColor, + onTriggered = + rememberCallback { + model.reduce( + FilteredContentsMviModel.Intent.SavePost( + post.id, + ), + ) + }, + ) + + else -> null + } + } + SwipeActionCard( + modifier = Modifier.fillMaxWidth(), + enabled = uiState.swipeActionsEnabled, + onGestureBegin = + rememberCallback(model) { + model.reduce(FilteredContentsMviModel.Intent.HapticIndication) + }, + swipeToStartActions = uiState.actionsOnSwipeToStartPosts.toSwipeActions(), + swipeToEndActions = uiState.actionsOnSwipeToEndPosts.toSwipeActions(), + content = { + PostCard( + post = post, + postLayout = uiState.postLayout, + limitBodyHeight = true, + fullHeightImage = uiState.fullHeightImages, + fullWidthImage = uiState.fullWidthImages, + voteFormat = uiState.voteFormat, + autoLoadImages = uiState.autoLoadImages, + preferNicknames = uiState.preferNicknames, + fadeRead = uiState.fadeReadPosts, + showUnreadComments = uiState.showUnreadComments, + downVoteEnabled = uiState.downVoteEnabled, + onClick = + rememberCallback(model) { + model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) + detailOpener.openPostDetail(post) + }, + onOpenCommunity = + rememberCallbackArgs { community, instance -> + detailOpener.openCommunityDetail( + community, + instance, + ) + }, + onOpenCreator = + rememberCallbackArgs { user, instance -> + detailOpener.openUserDetail(user, instance) + }, + onOpenPost = + rememberCallbackArgs { p, instance -> + detailOpener.openPostDetail(p, instance) + }, + onOpenWeb = + rememberCallbackArgs { url -> + navigationCoordinator.pushScreen( + WebViewScreen(url), + ) + }, + onUpVote = + rememberCallback(model) { + model.reduce( + FilteredContentsMviModel.Intent.UpVotePost(post.id), + ) + }, + onDownVote = + rememberCallback(model) { + model.reduce( + FilteredContentsMviModel.Intent.DownVotePost( + post.id, + ), + ) + }, + onSave = + rememberCallback(model) { + model.reduce( + FilteredContentsMviModel.Intent.SavePost(post.id), + ) + }, + onReply = + rememberCallback(model) { + model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) + detailOpener.openPostDetail(post) + }, + onOpenImage = + rememberCallbackArgs(model, post) { url -> + navigationCoordinator.pushScreen( + ZoomableImageScreen( + url = url, + source = post.community?.readableHandle.orEmpty(), + ), + ) + }, + options = + buildList { + this += + Option( + OptionId.SeeRaw, + LocalStrings.current.postActionSeeRaw, + ) + if (uiState.contentsType == FilteredContentsType.Moderated) { + this += + Option( + OptionId.FeaturePost, + if (post.featuredCommunity) { + LocalStrings.current.modActionUnmarkAsFeatured + } else { + LocalStrings.current.modActionMarkAsFeatured + }, + ) + this += + Option( + OptionId.LockPost, + if (post.locked) { + LocalStrings.current.modActionUnlock + } else { + LocalStrings.current.modActionLock + }, + ) + this += + Option( + OptionId.BanUser, + if (post.creator?.banned == true) { + LocalStrings.current.modActionAllow + } else { + LocalStrings.current.modActionBan + }, + ) + this += + Option( + OptionId.Remove, + LocalStrings.current.modActionRemove, + ) + } + if (uiState.isAdmin && uiState.contentsType == FilteredContentsType.Moderated) { + this += + Option( + OptionId.Purge, + LocalStrings.current.adminActionPurge, + ) + post.creator?.also { creator -> + this += + Option( + OptionId.PurgeCreator, + buildString { + append(LocalStrings.current.adminActionPurge) + append(" ") + append(creator.readableName(uiState.preferNicknames)) + }, + ) + } + this += + Option( + OptionId.AdminFeaturePost, + if (post.featuredLocal) { + LocalStrings.current.adminActionUnmarkAsFeatured + } else { + LocalStrings.current.adminActionMarkAsFeatured + }, + ) + } + }, + onOptionSelected = + rememberCallbackArgs(model) { optionId -> + when (optionId) { + OptionId.SeeRaw -> { + rawContent = post + } + + OptionId.FeaturePost -> + model.reduce( + FilteredContentsMviModel.Intent.ModFeaturePost( + post.id, + ), + ) + + OptionId.AdminFeaturePost -> + model.reduce( + FilteredContentsMviModel.Intent.AdminFeaturePost( + post.id, + ), + ) + + OptionId.LockPost -> + model.reduce( + FilteredContentsMviModel.Intent.ModLockPost( + post.id, + ), + ) + + OptionId.Remove -> { + val screen = + ModerateWithReasonScreen( + actionId = ModerateWithReasonAction.RemovePost.toInt(), + contentId = post.id, + ) + navigationCoordinator.pushScreen(screen) + } + + OptionId.BanUser -> { + post.creator?.id?.also { userId -> + post.community?.id?.also { communityId -> + val screen = + BanUserScreen( + userId = userId, + communityId = communityId, + newValue = post.creator?.banned != true, + postId = post.id, + ) + navigationCoordinator.pushScreen(screen) + } + } + } + + OptionId.Purge -> { + val screen = + ModerateWithReasonScreen( + actionId = ModerateWithReasonAction.PurgePost.toInt(), + contentId = post.id, + ) + navigationCoordinator.pushScreen(screen) + } + + OptionId.PurgeCreator -> { + post.creator?.id?.also { userId -> + val screen = + ModerateWithReasonScreen( + actionId = ModerateWithReasonAction.PurgeUser.toInt(), + contentId = userId, + ) + navigationCoordinator.pushScreen(screen) + } + } + + else -> Unit + } + }, + ) + }, + ) + + if (uiState.postLayout != PostLayout.Card) { + HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem)) + } else { + Spacer(modifier = Modifier.height(Spacing.interItem)) + } + } + } else { + if (uiState.comments.isEmpty() && uiState.loading && uiState.initial) { + items(5) { + ModdedCommentPlaceholder(postLayout = uiState.postLayout) + if (uiState.postLayout != PostLayout.Card) { + HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem)) + } else { + Spacer(modifier = Modifier.height(Spacing.interItem)) + } + } + } + if (uiState.comments.isEmpty() && !uiState.initial && !uiState.loading) { + item { + Text( + modifier = Modifier.fillMaxWidth().padding(top = Spacing.xs), + textAlign = TextAlign.Center, + text = LocalStrings.current.messageEmptyList, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + } + } + items( + uiState.comments, + { it.id.toString() + (it.updateDate ?: it.publishDate) }, + ) { comment -> + + @Composable + fun List.toSwipeActions(): List = + mapNotNull { + when (it) { + ActionOnSwipe.UpVote -> + SwipeAction( + swipeContent = { + Icon( + imageVector = Icons.Default.ArrowCircleUp, + contentDescription = null, + tint = Color.White, + ) + }, + backgroundColor = upVoteColor ?: defaultUpvoteColor, + onTriggered = + rememberCallback { + model.reduce( + FilteredContentsMviModel.Intent.UpVoteComment( + comment.id, + ), + ) + }, + ) + + ActionOnSwipe.DownVote -> + if (!uiState.downVoteEnabled) { + null + } else { + SwipeAction( + swipeContent = { + Icon( + imageVector = Icons.Default.ArrowCircleDown, + contentDescription = null, + tint = Color.White, + ) + }, + backgroundColor = + downVoteColor + ?: defaultDownVoteColor, + onTriggered = + rememberCallback { + model.reduce( + FilteredContentsMviModel.Intent.DownVoteComment( + comment.id, + ), + ) + }, + ) + } + + ActionOnSwipe.Reply -> + SwipeAction( + swipeContent = { + Icon( + imageVector = Icons.AutoMirrored.Default.Reply, + contentDescription = null, + tint = Color.White, + ) + }, + backgroundColor = replyColor ?: defaultReplyColor, + onTriggered = + rememberCallback { + detailOpener.openReply( + originalPost = PostModel(comment.postId), + originalComment = comment, + ) + }, + ) + + ActionOnSwipe.Save -> + SwipeAction( + swipeContent = { + Icon( + imageVector = Icons.Default.Bookmark, + contentDescription = null, + tint = Color.White, + ) + }, + backgroundColor = saveColor ?: defaultSaveColor, + onTriggered = + rememberCallback { + model.reduce( + FilteredContentsMviModel.Intent.SaveComment( + comment.id, + ), + ) + }, + ) + + else -> null + } + } + + SwipeActionCard( + modifier = Modifier.fillMaxWidth(), + enabled = uiState.swipeActionsEnabled, + onGestureBegin = + rememberCallback(model) { + model.reduce(FilteredContentsMviModel.Intent.HapticIndication) + }, + swipeToStartActions = uiState.actionsOnSwipeToStartComments.toSwipeActions(), + swipeToEndActions = uiState.actionsOnSwipeToEndComments.toSwipeActions(), + content = { + ModdedCommentCard( + comment = comment, + postLayout = uiState.postLayout, + voteFormat = uiState.voteFormat, + autoLoadImages = uiState.autoLoadImages, + preferNicknames = uiState.preferNicknames, + downVoteEnabled = uiState.downVoteEnabled, + onOpenUser = + rememberCallbackArgs { user, instance -> + detailOpener.openUserDetail(user, instance) + }, + onOpen = + rememberCallback { + model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) + detailOpener.openPostDetail( + post = PostModel(id = comment.postId), + highlightCommentId = comment.id, + isMod = true, + ) + }, + onUpVote = + rememberCallback(model) { + model.reduce( + FilteredContentsMviModel.Intent.UpVoteComment( + comment.id, + ), + ) + }, + onDownVote = + rememberCallback(model) { + model.reduce( + FilteredContentsMviModel.Intent.DownVoteComment( + comment.id, + ), + ) + }, + onSave = + rememberCallback(model) { + model.reduce( + FilteredContentsMviModel.Intent.SaveComment(comment.id), + ) + }, + onReply = + rememberCallback { + detailOpener.openReply( + originalPost = PostModel(id = comment.postId), + originalComment = comment, + ) + }, + options = + buildList { + this += + Option( + OptionId.SeeRaw, + LocalStrings.current.postActionSeeRaw, + ) + if (uiState.contentsType == FilteredContentsType.Moderated) { + this += + Option( + OptionId.DistinguishComment, + if (comment.distinguished) { + LocalStrings.current.modActionUnmarkAsDistinguished + } else { + LocalStrings.current.modActionMarkAsDistinguished + }, + ) + this += + Option( + OptionId.BanUser, + if (comment.creator?.banned == true) { + LocalStrings.current.modActionAllow + } else { + LocalStrings.current.modActionBan + }, + ) + this += + Option( + OptionId.Remove, + LocalStrings.current.modActionRemove, + ) + } + if (uiState.isAdmin && uiState.contentsType == FilteredContentsType.Moderated) { + this += + Option( + OptionId.Purge, + LocalStrings.current.adminActionPurge, + ) + comment.creator?.also { creator -> + this += + Option( + OptionId.PurgeCreator, + buildString { + append(LocalStrings.current.adminActionPurge) + append(" ") + append(creator.readableName(uiState.preferNicknames)) + }, + ) + } + } + }, + onOptionSelected = + rememberCallbackArgs { optionId -> + when (optionId) { + OptionId.Remove -> { + val screen = + ModerateWithReasonScreen( + actionId = ModerateWithReasonAction.RemoveComment.toInt(), + contentId = comment.id, + ) + navigationCoordinator.pushScreen(screen) + } + + OptionId.SeeRaw -> { + rawContent = comment + } + + OptionId.DistinguishComment -> + model.reduce( + FilteredContentsMviModel.Intent.ModDistinguishComment( + comment.id, + ), + ) + + OptionId.BanUser -> { + comment.creator?.id?.also { userId -> + comment.community?.id?.also { communityId -> + val screen = + BanUserScreen( + userId = userId, + communityId = communityId, + newValue = comment.creator?.banned != true, + commentId = comment.id, + ) + navigationCoordinator.pushScreen( + screen, + ) + } + } + } + + OptionId.Purge -> { + val screen = + ModerateWithReasonScreen( + actionId = ModerateWithReasonAction.PurgeComment.toInt(), + contentId = comment.id, + ) + navigationCoordinator.pushScreen(screen) + } + + OptionId.PurgeCreator -> { + comment.creator?.id?.also { userId -> + val screen = + ModerateWithReasonScreen( + actionId = ModerateWithReasonAction.PurgeUser.toInt(), + contentId = userId, + ) + navigationCoordinator.pushScreen(screen) + } + } + + else -> Unit + } + }, + ) + }, + ) + + if (uiState.postLayout != PostLayout.Card) { + HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem)) + } else { + Spacer(modifier = Modifier.height(Spacing.interItem)) + } + } + } + + item { + if (!uiState.initial && !uiState.loading && !uiState.refreshing && uiState.canFetchMore) { + model.reduce(FilteredContentsMviModel.Intent.LoadNextPage) + } + if (uiState.loading && !uiState.refreshing) { + Box( + modifier = Modifier.fillMaxWidth().padding(Spacing.xs), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator( + modifier = Modifier.size(25.dp), + color = MaterialTheme.colorScheme.primary, + ) + } + } + } + item { + Spacer(modifier = Modifier.height(Spacing.xxxl)) + } } + + PullRefreshIndicator( + refreshing = uiState.refreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + backgroundColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + ) } if (rawContent != null) {