refactor: use filtered contents to render bookmarks (#840)

This commit is contained in:
Diego Beraldin 2024-05-13 07:56:31 +02:00 committed by GitHub
parent 236590c0f9
commit a10189d212
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 18 additions and 1169 deletions

View File

@ -70,7 +70,6 @@ kotlin {
implementation(projects.unit.modlog)
implementation(projects.unit.myaccount)
implementation(projects.unit.reportlist)
implementation(projects.unit.saveditems)
implementation(projects.unit.web)
implementation(projects.unit.zoomableimage)
}

View File

@ -61,7 +61,6 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.managesubscriptions.ManageS
import com.github.diegoberaldin.raccoonforlemmy.unit.modlog.ModlogScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.myaccount.ProfileLoggedScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.reportlist.ReportListScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.saveditems.SavedItemsScreen
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
@ -125,7 +124,8 @@ internal object ProfileMainScreen : Tab {
}
NotificationCenterEvent.ProfileSideMenuAction.Bookmarks -> {
navigationCoordinator.pushScreen(SavedItemsScreen())
val screen = FilteredContentsScreen(type = FilteredContentsType.Bookmarks.toInt())
navigationCoordinator.pushScreen(screen)
}
NotificationCenterEvent.ProfileSideMenuAction.Drafts -> {

View File

@ -85,7 +85,6 @@ include(":unit:rawcontent")
include(":unit:remove")
include(":unit:replies")
include(":unit:reportlist")
include(":unit:saveditems")
include(":unit:selectcommunity")
include(":unit:selectinstance")
include(":unit:userdetail")

View File

@ -87,7 +87,6 @@ kotlin {
implementation(projects.unit.postdetail)
implementation(projects.unit.remove)
implementation(projects.unit.reportlist)
implementation(projects.unit.saveditems)
implementation(projects.unit.selectcommunity)
implementation(projects.unit.selectinstance)
implementation(projects.unit.userdetail)

View File

@ -50,7 +50,6 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.modlog.di.modlogModule
import com.github.diegoberaldin.raccoonforlemmy.unit.postdetail.di.postDetailModule
import com.github.diegoberaldin.raccoonforlemmy.unit.remove.di.removeModule
import com.github.diegoberaldin.raccoonforlemmy.unit.reportlist.di.reportListModule
import com.github.diegoberaldin.raccoonforlemmy.unit.saveditems.di.savedItemsModule
import com.github.diegoberaldin.raccoonforlemmy.unit.selectcommunity.di.selectCommunityModule
import com.github.diegoberaldin.raccoonforlemmy.unit.selectinstance.di.selectInstanceModule
import com.github.diegoberaldin.raccoonforlemmy.unit.userdetail.di.userDetailModule
@ -93,7 +92,6 @@ val sharedHelperModule = module {
instanceInfoModule,
removeModule,
reportListModule,
savedItemsModule,
createReportModule,
createPostModule,
createCommentModule,

View File

@ -51,7 +51,6 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.modlog.di.modlogModule
import com.github.diegoberaldin.raccoonforlemmy.unit.postdetail.di.postDetailModule
import com.github.diegoberaldin.raccoonforlemmy.unit.remove.di.removeModule
import com.github.diegoberaldin.raccoonforlemmy.unit.reportlist.di.reportListModule
import com.github.diegoberaldin.raccoonforlemmy.unit.saveditems.di.savedItemsModule
import com.github.diegoberaldin.raccoonforlemmy.unit.selectcommunity.di.selectCommunityModule
import com.github.diegoberaldin.raccoonforlemmy.unit.selectinstance.di.selectInstanceModule
import com.github.diegoberaldin.raccoonforlemmy.unit.userdetail.di.userDetailModule
@ -96,7 +95,6 @@ fun initKoin() {
instanceInfoModule,
removeModule,
reportListModule,
savedItemsModule,
createReportModule,
createPostModule,
createCommentModule,

View File

@ -11,14 +11,17 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
sealed interface FilteredContentsType {
data object Votes : FilteredContentsType
data object Moderated : FilteredContentsType
data object Bookmarks : FilteredContentsType
}
fun FilteredContentsType.toInt(): Int = when (this) {
FilteredContentsType.Moderated -> 0
FilteredContentsType.Votes -> 1
FilteredContentsType.Bookmarks -> 2
}
fun Int.toFilteredContentsType(): FilteredContentsType = when (this) {
2 -> FilteredContentsType.Bookmarks
1 -> FilteredContentsType.Votes
else -> FilteredContentsType.Moderated
}
@ -26,7 +29,6 @@ fun Int.toFilteredContentsType(): FilteredContentsType = when (this) {
sealed interface FilteredContentsSection {
data object Posts : FilteredContentsSection
data object Comments : FilteredContentsSection
}
@ -47,7 +49,7 @@ interface FilteredContentsMviModel :
data class ModFeaturePost(val id: Long) : Intent
data class ModLockPost(val id: Long) : Intent
data class ModDistinguishComment(val commentId: Long) : Intent
data object WillOpenDetail: Intent
data object WillOpenDetail : Intent
}
data class State(

View File

@ -175,6 +175,7 @@ class FilteredContentsScreen(
text = when (uiState.contentsType) {
FilteredContentsType.Moderated -> LocalXmlStrings.current.moderatorZoneActionContents
FilteredContentsType.Votes -> LocalXmlStrings.current.profileUpvotesDownvotes
FilteredContentsType.Bookmarks -> LocalXmlStrings.current.navigationDrawerTitleBookmarks
},
style = MaterialTheme.typography.titleMedium,
)

View File

@ -21,8 +21,6 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.imageUrl
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.launchIn
@ -185,6 +183,10 @@ class FilteredContentsViewModel(
liked = currentState.liked,
sortType = SortType.New,
)
FilteredContentsType.Bookmarks -> PostPaginationSpecification.Saved(
sortType = SortType.New,
)
}
postPaginationManager.reset(postSpecification)
val commentSpecification = when (currentState.contentsType) {
@ -197,6 +199,10 @@ class FilteredContentsViewModel(
liked = currentState.liked,
sortType = SortType.New,
)
FilteredContentsType.Bookmarks -> CommentPaginationSpecification.Saved(
sortType = SortType.New,
)
}
commentPaginationManager.reset(commentSpecification)
updateState {
@ -329,7 +335,7 @@ class FilteredContentsViewModel(
saved = newValue,
)
handlePostUpdate(newPost)
screenModelScope.launch(Dispatchers.IO) {
screenModelScope.launch {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.save(
@ -348,7 +354,7 @@ class FilteredContentsViewModel(
}
private fun feature(post: PostModel) {
screenModelScope.launch(Dispatchers.IO) {
screenModelScope.launch {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postRepository.featureInCommunity(
postId = post.id, auth = auth, featured = !post.featuredCommunity
@ -360,7 +366,7 @@ class FilteredContentsViewModel(
}
private fun lock(post: PostModel) {
screenModelScope.launch(Dispatchers.IO) {
screenModelScope.launch {
val auth = identityRepository.authToken.value.orEmpty()
val newPost = postRepository.lock(
postId = post.id,

View File

@ -1,81 +0,0 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
alias(libs.plugins.compose)
alias(libs.plugins.detekt)
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
applyDefaultHierarchyTemplate()
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "saveditems"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.material3)
implementation(compose.materialIconsExtended)
implementation(libs.koin.core)
implementation(libs.voyager.navigator)
implementation(libs.voyager.screenmodel)
implementation(libs.voyager.koin)
implementation(projects.core.appearance)
implementation(projects.core.architecture)
implementation(projects.core.commonui.components)
implementation(projects.core.commonui.detailopenerApi)
implementation(projects.core.commonui.lemmyui)
implementation(projects.core.commonui.modals)
implementation(projects.core.l10n)
implementation(projects.core.navigation)
implementation(projects.core.notifications)
implementation(projects.core.persistence)
implementation(projects.core.utils)
implementation(projects.domain.identity)
implementation(projects.domain.lemmy.data)
implementation(projects.domain.lemmy.pagination)
implementation(projects.domain.lemmy.repository)
implementation(projects.unit.zoomableimage)
implementation(projects.unit.web)
implementation(projects.unit.createreport)
implementation(projects.unit.createcomment)
implementation(projects.unit.rawcontent)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
android {
namespace = "com.github.diegoberaldin.raccoonforlemmy.unit.saveditems"
compileSdk = libs.versions.android.targetSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
}

View File

@ -1,13 +0,0 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues></ManuallySuppressedIssues>
<CurrentIssues>
<ID>CyclomaticComplexMethod:SavedItemsScreen.kt$SavedItemsScreen$@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable override fun Content()</ID>
<ID>CyclomaticComplexMethod:SavedItemsViewModel.kt$SavedItemsViewModel$override fun reduce(intent: SavedItemsMviModel.Intent)</ID>
<ID>LongMethod:SavedItemsScreen.kt$SavedItemsScreen$@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Composable override fun Content()</ID>
<ID>LongMethod:SavedItemsViewModel.kt$SavedItemsViewModel$private fun loadNextPage()</ID>
<ID>LongParameterList:SavedItemsViewModel.kt$SavedItemsViewModel$( private val identityRepository: IdentityRepository, private val apiConfigurationRepository: ApiConfigurationRepository, private val siteRepository: SiteRepository, private val userRepository: UserRepository, private val postRepository: PostRepository, private val commentRepository: CommentRepository, private val themeRepository: ThemeRepository, private val settingsRepository: SettingsRepository, private val shareHelper: ShareHelper, private val notificationCenter: NotificationCenter, private val hapticFeedback: HapticFeedback, private val getSortTypesUseCase: GetSortTypesUseCase, )</ID>
<ID>TooGenericExceptionCaught:SavedItemsViewModel.kt$SavedItemsViewModel$e: Throwable</ID>
<ID>TooManyFunctions:SavedItemsViewModel.kt$SavedItemsViewModel : SavedItemsMviModelDefaultMviModel</ID>
</CurrentIssues>
</SmellBaseline>

View File

@ -1,56 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.saveditems
import androidx.compose.runtime.Stable
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.VoteFormat
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
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.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
@Stable
interface SavedItemsMviModel :
MviModel<SavedItemsMviModel.Intent, SavedItemsMviModel.UiState, SavedItemsMviModel.Effect>,
ScreenModel {
sealed interface Intent {
data object Refresh : Intent
data object LoadNextPage : Intent
data class ChangeSection(val section: SavedItemsSection) : Intent
data class UpVotePost(val id: Long, val feedback: Boolean = false) : Intent
data class DownVotePost(val id: Long, val feedback: Boolean = false) : Intent
data class SavePost(val id: Long, val feedback: Boolean = false) : Intent
data class UpVoteComment(val id: Long, val feedback: Boolean = false) : Intent
data class DownVoteComment(val id: Long, val feedback: Boolean = false) : Intent
data class SaveComment(val id: Long, val feedback: Boolean = false) : Intent
data class Share(val url: String) : Intent
data object WillOpenSave : Intent
}
data class UiState(
val section: SavedItemsSection = SavedItemsSection.Posts,
val user: UserModel? = null,
val instance: String = "",
val refreshing: Boolean = false,
val loading: Boolean = false,
val canFetchMore: Boolean = true,
val sortType: SortType = SortType.New,
val blurNsfw: Boolean = true,
val posts: List<PostModel> = emptyList(),
val comments: List<CommentModel> = emptyList(),
val postLayout: PostLayout = PostLayout.Card,
val fullHeightImages: Boolean = true,
val fullWidthImages: Boolean = false,
val voteFormat: VoteFormat = VoteFormat.Aggregated,
val autoLoadImages: Boolean = true,
val preferNicknames: Boolean = true,
val showScores: Boolean = true,
val availableSortTypes: List<SortType> = emptyList(),
)
sealed interface Effect {
data object BackToTop : Effect
}
}

View File

@ -1,565 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.saveditems
import androidx.compose.animation.AnimatedVisibility
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
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.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.FloatingActionButtonMenu
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.FloatingActionButtonMenuItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.detailopener.api.getDetailOpener
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CommentCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.Option
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.OptionId
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.PostCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.di.getFabNestedScrollConnection
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.ShareBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.LocalXmlStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
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.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.readableHandle
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toIcon
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toInt
import com.github.diegoberaldin.raccoonforlemmy.unit.createreport.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.rawcontent.RawContentDialog
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.ZoomableImageScreen
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class SavedItemsScreen : Screen {
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = getScreenModel<SavedItemsMviModel>()
val uiState by model.uiState.collectAsState()
val navigatorCoordinator = remember { getNavigationCoordinator() }
val topAppBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState)
val lazyListState = rememberLazyListState()
val scope = rememberCoroutineScope()
val fabNestedScrollConnection = remember { getFabNestedScrollConnection() }
val isFabVisible by fabNestedScrollConnection.isFabVisible.collectAsState()
var rawContent by remember { mutableStateOf<Any?>(null) }
val settingsRepository = remember { getSettingsRepository() }
val settings by settingsRepository.currentSettings.collectAsState()
val navigationCoordinator = remember { getNavigationCoordinator() }
val detailOpener = remember { getDetailOpener() }
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.globalMessage.onEach { message ->
snackbarHostState.showSnackbar(
message = message,
)
}.launchIn(this)
}
Scaffold(
modifier = Modifier.background(MaterialTheme.colorScheme.background),
topBar = {
TopAppBar(
scrollBehavior = scrollBehavior,
title = {
Text(
modifier = Modifier.padding(horizontal = Spacing.s),
text = LocalXmlStrings.current.navigationDrawerTitleBookmarks,
style = MaterialTheme.typography.titleMedium,
)
},
actions = {
Image(
modifier = Modifier
.padding(horizontal = Spacing.xs)
.onClick(
onClick = {
val sheet = SortBottomSheet(
values = uiState.availableSortTypes.map { it.toInt() },
screenKey = "savedItems",
)
navigatorCoordinator.showBottomSheet(sheet)
},
),
imageVector = uiState.sortType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
},
navigationIcon = {
Image(
modifier = Modifier.onClick(
onClick = {
navigatorCoordinator.popScreen()
},
),
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
},
)
},
floatingActionButton = {
AnimatedVisibility(
visible = isFabVisible,
enter = slideInVertically(
initialOffsetY = { it * 2 },
),
exit = slideOutVertically(
targetOffsetY = { it * 2 },
),
) {
FloatingActionButtonMenu(
items = buildList {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.ExpandLess,
text = LocalXmlStrings.current.actionBackToTop,
onSelected = rememberCallback {
scope.launch {
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)
},
)
}
},
snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
snackbarData = data,
)
}
},
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.then(
if (settings.hideNavigationBarWhileScrolling) {
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
Modifier
}
).nestedScroll(fabNestedScrollConnection),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
SectionSelector(
titles = listOf(
LocalXmlStrings.current.profileSectionPosts,
LocalXmlStrings.current.profileSectionComments,
),
currentSection = when (uiState.section) {
SavedItemsSection.Comments -> 1
else -> 0
},
onSectionSelected = {
val section = when (it) {
1 -> SavedItemsSection.Comments
else -> SavedItemsSection.Posts
}
model.reduce(SavedItemsMviModel.Intent.ChangeSection(section))
},
)
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.refreshing,
onRefresh = rememberCallback(model) {
model.reduce(SavedItemsMviModel.Intent.Refresh)
},
)
Box(
modifier = Modifier.fillMaxWidth().pullRefresh(pullRefreshState),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = lazyListState,
) {
if (uiState.section == SavedItemsSection.Posts) {
items(uiState.posts) { post ->
PostCard(
post = post,
postLayout = uiState.postLayout,
limitBodyHeight = true,
fullHeightImage = uiState.fullHeightImages,
fullWidthImage = uiState.fullWidthImages,
voteFormat = uiState.voteFormat,
autoLoadImages = uiState.autoLoadImages,
preferNicknames = uiState.preferNicknames,
showScores = uiState.showScores,
blurNsfw = uiState.blurNsfw,
onClick = {
model.reduce(SavedItemsMviModel.Intent.WillOpenSave)
detailOpener.openPostDetail(post)
},
onOpenCommunity = rememberCallbackArgs { community, instance ->
detailOpener.openCommunityDetail(community, instance)
},
onOpenCreator = rememberCallbackArgs { u, instance ->
if (u.id != uiState.user?.id) {
detailOpener.openUserDetail(u, instance)
}
},
onOpenPost = rememberCallbackArgs { p, instance ->
detailOpener.openPostDetail(p, instance)
},
onOpenWeb = rememberCallbackArgs { url ->
navigationCoordinator.pushScreen(
WebViewScreen(url)
)
},
onUpVote = rememberCallback(model) {
model.reduce(
SavedItemsMviModel.Intent.UpVotePost(
id = post.id,
),
)
},
onDownVote = rememberCallback(model) {
model.reduce(
SavedItemsMviModel.Intent.DownVotePost(
id = post.id,
),
)
},
onSave = rememberCallback(model) {
model.reduce(
SavedItemsMviModel.Intent.SavePost(
id = post.id,
),
)
},
onReply = rememberCallback {
model.reduce(SavedItemsMviModel.Intent.WillOpenSave)
detailOpener.openPostDetail(post)
},
onOpenImage = rememberCallbackArgs { url ->
navigatorCoordinator.pushScreen(
ZoomableImageScreen(
url = url,
source = post.community?.readableHandle.orEmpty(),
),
)
},
options = buildList {
add(
Option(
OptionId.Share,
LocalXmlStrings.current.postActionShare
)
)
add(
Option(
OptionId.SeeRaw,
LocalXmlStrings.current.postActionSeeRaw
)
)
add(
Option(
OptionId.Report,
LocalXmlStrings.current.postActionReport
)
)
},
onOptionSelected = { optionIndex ->
when (optionIndex) {
OptionId.Report -> {
navigatorCoordinator.pushScreen(
CreateReportScreen(postId = post.id),
)
}
OptionId.SeeRaw -> {
rawContent = post
}
OptionId.Share -> {
val urls = listOfNotNull(
post.originalUrl,
"https://${uiState.instance}/post/${post.id}"
).distinct()
if (urls.size == 1) {
model.reduce(
SavedItemsMviModel.Intent.Share(
urls.first()
)
)
} else {
val screen = ShareBottomSheet(urls = urls)
navigationCoordinator.showBottomSheet(screen)
}
}
else -> Unit
}
},
)
if (uiState.postLayout != PostLayout.Card) {
HorizontalDivider(modifier = Modifier.padding(vertical = Spacing.interItem))
} else {
Spacer(modifier = Modifier.height(Spacing.interItem))
}
}
if (uiState.posts.isEmpty() && !uiState.loading) {
item {
Text(
modifier = Modifier.fillMaxWidth().padding(top = Spacing.xs),
textAlign = TextAlign.Center,
text = LocalXmlStrings.current.messageEmptyList,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
} else {
items(uiState.comments) { comment ->
CommentCard(
comment = comment,
voteFormat = uiState.voteFormat,
autoLoadImages = uiState.autoLoadImages,
preferNicknames = uiState.preferNicknames,
showScores = uiState.showScores,
showBot = true,
indentAmount = 0,
onClick = {
detailOpener.openPostDetail(
post = PostModel(id = comment.postId),
highlightCommentId = comment.id,
)
},
onImageClick = rememberCallbackArgs { url ->
navigationCoordinator.pushScreen(
ZoomableImageScreen(
url = url,
source = comment.community?.readableHandle.orEmpty(),
)
)
},
onUpVote = {
model.reduce(
SavedItemsMviModel.Intent.UpVoteComment(
id = comment.id,
),
)
},
onDownVote = {
model.reduce(
SavedItemsMviModel.Intent.DownVoteComment(
id = comment.id,
),
)
},
onSave = {
model.reduce(
SavedItemsMviModel.Intent.SaveComment(
id = comment.id,
),
)
},
onReply = {
detailOpener.openReply(
originalPost = PostModel(id = comment.postId),
originalComment = comment,
)
},
options = buildList {
add(
Option(
OptionId.SeeRaw,
LocalXmlStrings.current.postActionSeeRaw
)
)
add(
Option(
OptionId.Report,
LocalXmlStrings.current.postActionReport
)
)
},
onOptionSelected = { optionIndex ->
when (optionIndex) {
OptionId.Report -> {
navigatorCoordinator.pushScreen(
CreateReportScreen(commentId = comment.id),
)
}
OptionId.SeeRaw -> {
rawContent = comment
}
else -> Unit
}
},
)
HorizontalDivider(
modifier = Modifier.padding(vertical = Spacing.xxxs),
thickness = 0.25.dp
)
}
if (uiState.comments.isEmpty() && !uiState.loading) {
item {
Text(
modifier = Modifier.fillMaxWidth().padding(top = Spacing.xs),
textAlign = TextAlign.Center,
text = LocalXmlStrings.current.messageEmptyList,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(SavedItemsMviModel.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,
)
}
}
}
}
PullRefreshIndicator(
refreshing = uiState.refreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
)
}
}
}
if (rawContent != null) {
when (val content = rawContent) {
is PostModel -> {
RawContentDialog(
title = content.title,
publishDate = content.publishDate,
updateDate = content.updateDate,
url = content.url,
text = content.text,
upVotes = content.upvotes,
downVotes = content.downvotes,
onDismiss = rememberCallback {
rawContent = null
},
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
detailOpener.openReply(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
},
)
}
is CommentModel -> {
RawContentDialog(
text = content.text,
publishDate = content.publishDate,
updateDate = content.updateDate,
upVotes = content.upvotes,
downVotes = content.downvotes,
onDismiss = {
rawContent = null
},
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
detailOpener.openReply(
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
}
)
}
}
}
}
}

View File

@ -1,7 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.saveditems
sealed interface SavedItemsSection {
data object Posts : SavedItemsSection
data object Comments : SavedItemsSection
}

View File

@ -1,406 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.saveditems
import cafe.adriel.voyager.core.model.screenModelScope
import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.CommentPaginationManager
import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.CommentPaginationSpecification
import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.PostNavigationManager
import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.PostPaginationManager
import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.PostPaginationSpecification
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.share.ShareHelper
import com.github.diegoberaldin.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
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.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.GetSortTypesUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class SavedItemsViewModel(
private val identityRepository: IdentityRepository,
private val apiConfigurationRepository: ApiConfigurationRepository,
private val postPaginationManager: PostPaginationManager,
private val commentPaginationManager: CommentPaginationManager,
private val postRepository: PostRepository,
private val commentRepository: CommentRepository,
private val themeRepository: ThemeRepository,
private val settingsRepository: SettingsRepository,
private val shareHelper: ShareHelper,
private val notificationCenter: NotificationCenter,
private val hapticFeedback: HapticFeedback,
private val getSortTypesUseCase: GetSortTypesUseCase,
private val postNavigationManager: PostNavigationManager,
) : SavedItemsMviModel,
DefaultMviModel<SavedItemsMviModel.Intent, SavedItemsMviModel.UiState, SavedItemsMviModel.Effect>(
initialState = SavedItemsMviModel.UiState(),
) {
init {
updateState { it.copy(instance = apiConfigurationRepository.instance.value) }
screenModelScope.launch {
themeRepository.postLayout.onEach { layout ->
updateState { it.copy(postLayout = layout) }
}.launchIn(this)
settingsRepository.currentSettings.onEach { settings ->
updateState {
it.copy(
voteFormat = settings.voteFormat,
autoLoadImages = settings.autoLoadImages,
preferNicknames = settings.preferUserNicknames,
fullHeightImages = settings.fullHeightImages,
fullWidthImages = settings.fullWidthImages,
showScores = settings.showScores,
)
}
}.launchIn(this)
notificationCenter.subscribe(NotificationCenterEvent.PostUpdated::class).onEach { evt ->
handlePostUpdate(evt.model)
}.launchIn(this)
notificationCenter.subscribe(NotificationCenterEvent.PostDeleted::class).onEach { evt ->
handlePostDelete(evt.model.id)
}.launchIn(this)
notificationCenter.subscribe(NotificationCenterEvent.ChangeSortType::class)
.onEach { evt ->
if (evt.screenKey == "savedItems") {
applySortType(evt.value)
}
}.launchIn(this)
notificationCenter.subscribe(NotificationCenterEvent.Share::class).onEach { evt ->
shareHelper.share(evt.url)
}.launchIn(this)
if (uiState.value.posts.isEmpty()) {
val sortTypes = getSortTypesUseCase.getTypesForSavedItems()
updateState { it.copy(availableSortTypes = sortTypes) }
refresh()
}
}
}
override fun reduce(intent: SavedItemsMviModel.Intent) {
when (intent) {
SavedItemsMviModel.Intent.LoadNextPage -> screenModelScope.launch {
loadNextPage()
}
SavedItemsMviModel.Intent.Refresh -> screenModelScope.launch {
refresh()
}
is SavedItemsMviModel.Intent.ChangeSection -> changeSection(intent.section)
is SavedItemsMviModel.Intent.DownVoteComment -> {
if (intent.feedback) {
hapticFeedback.vibrate()
}
toggleDownVoteComment(
comment = uiState.value.comments.first { it.id == intent.id },
)
}
is SavedItemsMviModel.Intent.DownVotePost -> {
if (intent.feedback) {
hapticFeedback.vibrate()
}
toggleDownVotePost(
post = uiState.value.posts.first { it.id == intent.id },
)
}
is SavedItemsMviModel.Intent.SaveComment -> {
if (intent.feedback) {
hapticFeedback.vibrate()
}
toggleSaveComment(
comment = uiState.value.comments.first { it.id == intent.id },
)
}
is SavedItemsMviModel.Intent.SavePost -> {
if (intent.feedback) {
hapticFeedback.vibrate()
}
toggleSavePost(
post = uiState.value.posts.first { it.id == intent.id },
)
}
is SavedItemsMviModel.Intent.Share -> {
shareHelper.share(intent.url)
}
is SavedItemsMviModel.Intent.UpVoteComment -> {
if (intent.feedback) {
hapticFeedback.vibrate()
}
toggleUpVoteComment(
comment = uiState.value.comments.first { it.id == intent.id },
)
}
is SavedItemsMviModel.Intent.UpVotePost -> {
if (intent.feedback) {
hapticFeedback.vibrate()
}
toggleUpVotePost(
post = uiState.value.posts.first { it.id == intent.id },
)
}
SavedItemsMviModel.Intent.WillOpenSave -> {
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
}
}
}
private suspend fun refresh() {
postPaginationManager.reset(
PostPaginationSpecification.Saved(sortType = uiState.value.sortType)
)
commentPaginationManager.reset(
CommentPaginationSpecification.Saved(sortType = uiState.value.sortType)
)
updateState {
it.copy(
canFetchMore = true,
refreshing = true,
loading = false,
)
}
loadNextPage()
}
private suspend fun loadNextPage() {
val currentState = uiState.value
if (!currentState.canFetchMore || currentState.loading) {
updateState { it.copy(refreshing = false) }
return
}
updateState { it.copy(loading = true) }
val section = currentState.section
if (section == SavedItemsSection.Posts) {
val posts = postPaginationManager.loadNextPage()
updateState {
it.copy(
posts = posts,
loading = false,
canFetchMore = postPaginationManager.canFetchMore,
refreshing = false,
)
}
} else {
val comments = commentPaginationManager.loadNextPage()
updateState {
it.copy(
comments = comments,
loading = false,
canFetchMore = commentPaginationManager.canFetchMore,
refreshing = false,
)
}
}
}
private fun applySortType(value: SortType) {
if (uiState.value.sortType == value) {
return
}
updateState { it.copy(sortType = value) }
screenModelScope.launch {
emitEffect(SavedItemsMviModel.Effect.BackToTop)
delay(50)
refresh()
}
}
private fun handlePostUpdate(post: PostModel) {
updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
private fun handleCommentUpdate(comment: CommentModel) {
updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
comment
} else {
c
}
},
)
}
}
private fun handlePostDelete(id: Long) {
updateState { it.copy(posts = it.posts.filter { post -> post.id != id }) }
}
private fun changeSection(section: SavedItemsSection) {
updateState {
it.copy(
section = section,
)
}
}
private fun toggleUpVotePost(post: PostModel) {
val newValue = post.myVote <= 0
val newPost = postRepository.asUpVoted(
post = post,
voted = newValue,
)
handlePostUpdate(newPost)
screenModelScope.launch {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.upVote(
auth = auth,
post = post,
voted = newValue,
)
notificationCenter.send(
event = NotificationCenterEvent.PostUpdated(newPost),
)
} catch (e: Throwable) {
e.printStackTrace()
handlePostUpdate(post)
}
}
}
private fun toggleDownVotePost(post: PostModel) {
val newValue = post.myVote >= 0
val newPost = postRepository.asDownVoted(
post = post,
downVoted = newValue,
)
handlePostUpdate(newPost)
screenModelScope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.downVote(
auth = auth,
post = post,
downVoted = newValue,
)
notificationCenter.send(
event = NotificationCenterEvent.PostUpdated(newPost),
)
} catch (e: Throwable) {
e.printStackTrace()
handlePostUpdate(post)
}
}
}
private fun toggleSavePost(post: PostModel) {
val newValue = !post.saved
val newPost = postRepository.asSaved(
post = post,
saved = newValue,
)
handlePostUpdate(newPost)
screenModelScope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.save(
auth = auth,
post = post,
saved = newValue,
)
notificationCenter.send(
event = NotificationCenterEvent.PostUpdated(newPost),
)
} catch (e: Throwable) {
e.printStackTrace()
handlePostUpdate(post)
}
}
}
private fun toggleUpVoteComment(comment: CommentModel) {
val newValue = comment.myVote <= 0
val newComment = commentRepository.asUpVoted(
comment = comment,
voted = newValue,
)
handleCommentUpdate(newComment)
screenModelScope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.upVote(
auth = auth,
comment = comment,
voted = newValue,
)
} catch (e: Throwable) {
e.printStackTrace()
handleCommentUpdate(comment)
}
}
}
private fun toggleDownVoteComment(comment: CommentModel) {
val newValue = comment.myVote >= 0
val newComment = commentRepository.asDownVoted(comment, newValue)
handleCommentUpdate(newComment)
screenModelScope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.downVote(
auth = auth,
comment = comment,
downVoted = newValue,
)
} catch (e: Throwable) {
e.printStackTrace()
handleCommentUpdate(comment)
}
}
}
private fun toggleSaveComment(comment: CommentModel) {
val newValue = !comment.saved
val newComment = commentRepository.asSaved(
comment = comment,
saved = newValue,
)
handleCommentUpdate(newComment)
screenModelScope.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.save(
auth = auth,
comment = comment,
saved = newValue,
)
} catch (e: Throwable) {
e.printStackTrace()
handleCommentUpdate(comment)
}
}
}
}

View File

@ -1,25 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.saveditems.di
import com.github.diegoberaldin.raccoonforlemmy.unit.saveditems.SavedItemsMviModel
import com.github.diegoberaldin.raccoonforlemmy.unit.saveditems.SavedItemsViewModel
import org.koin.dsl.module
val savedItemsModule = module {
factory<SavedItemsMviModel> {
SavedItemsViewModel(
identityRepository = get(),
apiConfigurationRepository = get(),
postPaginationManager = get(),
commentPaginationManager = get(),
postRepository = get(),
commentRepository = get(),
themeRepository = get(),
settingsRepository = get(),
shareHelper = get(),
hapticFeedback = get(),
notificationCenter = get(),
getSortTypesUseCase = get(),
postNavigationManager = get(),
)
}
}