refactor(profile): move saved items to separate screen and restructure post and comments section (#23)

* feat(profile): move saved items to separate screen and introduce saved comments

* feat(profile): unifies posts and comments
This commit is contained in:
Diego Beraldin 2023-09-17 22:42:03 +02:00 committed by GitHub
parent 0eda8a341a
commit 2614e31b9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1179 additions and 861 deletions

View File

@ -9,6 +9,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmarks
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -18,6 +21,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
import com.github.diegoberaldin.racconforlemmy.core.utils.toLocalPixel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
@ -27,6 +31,7 @@ import io.kamel.image.asyncPainterResource
@Composable
fun UserHeader(
user: UserModel,
onOpenBookmarks: (() -> Unit)? = null,
) {
val userAvatar = user.avatar.orEmpty()
val userDisplayName = user.name
@ -97,5 +102,17 @@ fun UserHeader(
)
}
}
if (onOpenBookmarks != null) {
Icon(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(Spacing.s)
.onClick {
onOpenBookmarks.invoke()
},
imageVector = Icons.Outlined.Bookmarks,
contentDescription = null,
)
}
}
}

View File

@ -43,7 +43,6 @@ class UserRepository(
page: Int,
limit: Int = PostsRepository.DEFAULT_PAGE_SIZE,
sort: SortType = SortType.Active,
savedOnly: Boolean = false,
): List<PostModel> = runCatching {
val response = serviceProvider.user.getDetails(
auth = auth,
@ -51,7 +50,25 @@ class UserRepository(
page = page,
limit = limit,
sort = sort.toCommentDto(),
savedOnly = savedOnly,
)
val dto = response.body() ?: return@runCatching emptyList()
dto.posts.map { it.toModel() }
}.getOrElse { emptyList() }
suspend fun getSavedPosts(
id: Int,
auth: String? = null,
page: Int,
limit: Int = PostsRepository.DEFAULT_PAGE_SIZE,
sort: SortType = SortType.Active,
): List<PostModel> = runCatching {
val response = serviceProvider.user.getDetails(
auth = auth,
personId = id,
page = page,
limit = limit,
sort = sort.toCommentDto(),
savedOnly = true,
)
val dto = response.body() ?: return@runCatching emptyList()
dto.posts.map { it.toModel() }
@ -75,6 +92,25 @@ class UserRepository(
dto.comments.map { it.toModel() }
}.getOrElse { emptyList() }
suspend fun getSavedComments(
id: Int,
auth: String? = null,
page: Int,
limit: Int = PostsRepository.DEFAULT_PAGE_SIZE,
sort: SortType = SortType.Active,
): List<CommentModel> = runCatching {
val response = serviceProvider.user.getDetails(
auth = auth,
personId = id,
page = page,
limit = limit,
sort = sort.toCommentDto(),
savedOnly = true,
)
val dto = response.body() ?: return@runCatching emptyList()
dto.comments.map { it.toModel() }
}.getOrElse { emptyList() }
suspend fun getMentions(
auth: String? = null,
page: Int,

View File

@ -3,8 +3,7 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.profile.di
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import org.koin.core.parameter.parametersOf
import org.koin.java.KoinJavaComponent.inject
@ -24,20 +23,9 @@ actual fun getProfileLoggedViewModel(): ProfileLoggedViewModel {
return res
}
actual fun getProfilePostsViewModel(
user: UserModel,
savedOnly: Boolean,
): ProfilePostsViewModel {
val res: ProfilePostsViewModel by inject(
clazz = ProfilePostsViewModel::class.java,
parameters = { parametersOf(user, savedOnly) },
)
return res
}
actual fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewModel {
val res: ProfileCommentsViewModel by inject(
clazz = ProfileCommentsViewModel::class.java,
actual fun getProfileSavedViewModel(user: UserModel): ProfileSavedViewModel {
val res: ProfileSavedViewModel by inject(
clazz = ProfileSavedViewModel::class.java,
parameters = { parametersOf(user) },
)
return res

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement

View File

@ -1,18 +1,29 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
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.UserModel
interface ProfileLoggedMviModel :
MviModel<ProfileLoggedMviModel.Intent, ProfileLoggedMviModel.UiState, ProfileLoggedMviModel.Effect> {
sealed interface Intent {
data class SelectTab(val value: ProfileLoggedSection) : Intent
data class ChangeSection(val section: ProfileLoggedSection) : Intent
object Refresh : Intent
object LoadNextPage : Intent
data class DeletePost(val id: Int) : Intent
data class DeleteComment(val id: Int) : Intent
}
data class UiState(
val user: UserModel? = null,
val currentTab: ProfileLoggedSection = ProfileLoggedSection.POSTS,
val section: ProfileLoggedSection = ProfileLoggedSection.Posts,
val refreshing: Boolean = false,
val loading: Boolean = false,
val canFetchMore: Boolean = true,
val posts: List<PostModel> = emptyList(),
val comments: List<CommentModel> = emptyList(),
)
sealed interface Effect

View File

@ -1,34 +1,50 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
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.material.ExperimentalMaterialApi
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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.CurrentScreen
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
import com.github.diegoberaldin.racconforlemmy.core.utils.toLocalPixel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.UserCounters
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.UserHeader
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsScreen
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsScreen
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.saved.ProfileSavedScreen
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedScreen
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getProfileLoggedViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
internal object ProfileLoggedScreen : Tab {
@ -37,6 +53,7 @@ internal object ProfileLoggedScreen : Tab {
return TabOptions(0u, "")
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
override fun Content() {
Column(
@ -49,6 +66,7 @@ internal object ProfileLoggedScreen : Tab {
val uiState by model.uiState.collectAsState()
val user = uiState.user
val notificationCenter = remember { getNotificationCenter() }
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
DisposableEffect(key) {
onDispose {
notificationCenter.removeObserver(key)
@ -56,47 +74,133 @@ internal object ProfileLoggedScreen : Tab {
}
if (user != null) {
val screens = remember {
val postsScreen = ProfilePostsScreen(user)
val commentsScreen = ProfileCommentsScreen(user)
val savedScreen = ProfileSavedScreen(user)
notificationCenter.addObserver({
(it as? ProfileLoggedSection)?.also { value ->
model.reduce(ProfileLoggedMviModel.Intent.SelectTab(value))
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(ProfileLoggedMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
UserHeader(
user = user,
onOpenBookmarks = {
navigator?.push(
ProfileSavedScreen(user = user),
)
},
)
UserCounters(
modifier = Modifier.graphicsLayer(translationY = -Spacing.m.toLocalPixel()),
user = user,
)
Spacer(modifier = Modifier.height(Spacing.xs))
SectionSelector(
titles = listOf(
stringResource(MR.strings.profile_section_posts),
stringResource(MR.strings.profile_section_comments),
),
currentSection = when (uiState.section) {
ProfileLoggedSection.Comments -> 1
else -> 0
},
onSectionSelected = {
val section = when (it) {
1 -> ProfileLoggedSection.Comments
else -> ProfileLoggedSection.Posts
}
model.reduce(
ProfileLoggedMviModel.Intent.ChangeSection(
section
)
)
},
)
Spacer(modifier = Modifier.height(Spacing.m))
}
}
}, key, NotificationCenterContractKeys.SectionChanged)
notificationCenter.addObserver({
(it as? ProfileLoggedSection)?.also { value ->
model.reduce(ProfileLoggedMviModel.Intent.SelectTab(value))
if (uiState.section == ProfileLoggedSection.Posts) {
items(uiState.posts) { post ->
ProfilePostCard(
modifier = Modifier.onClick {
navigator?.push(
PostDetailScreen(post),
)
},
post = post,
options = listOf(stringResource(MR.strings.comment_action_delete)),
onOpenCommunity = { community ->
navigator?.push(
CommunityDetailScreen(community),
)
},
onImageClick = { url ->
navigator?.push(
ZoomableImageScreen(url),
)
},
onOptionSelected = { idx ->
when (idx) {
else -> model.reduce(
ProfileLoggedMviModel.Intent.DeletePost(post.id)
)
}
}
)
}
} else {
items(uiState.comments) { comment ->
ProfileCommentCard(
comment = comment,
options = listOf(stringResource(MR.strings.comment_action_delete)),
onOptionSelected = { idx ->
when (idx) {
else ->
model.reduce(
ProfileLoggedMviModel.Intent.DeleteComment(
comment.id
)
)
}
}
)
}
}
}, key, NotificationCenterContractKeys.SectionChanged)
notificationCenter.addObserver({
(it as? ProfileLoggedSection)?.also { value ->
model.reduce(ProfileLoggedMviModel.Intent.SelectTab(value))
item {
Spacer(modifier = Modifier.height(Spacing.xxxl))
}
}, key, NotificationCenterContractKeys.SectionChanged)
listOf(
postsScreen,
commentsScreen,
savedScreen,
)
}
TabNavigator(screens.first()) {
CurrentScreen()
val navigator = LocalTabNavigator.current
LaunchedEffect(model) {
model.uiState.map { it.currentTab }
.onEach { section ->
val index = when (section) {
ProfileLoggedSection.POSTS -> 0
ProfileLoggedSection.COMMENTS -> 1
ProfileLoggedSection.SAVED -> 2
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(ProfileLoggedMviModel.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,
)
}
navigator.current = screens[index]
}.launchIn(this)
}
}
}
PullRefreshIndicator(
refreshing = uiState.refreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
)
}
}
}

View File

@ -1,7 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
enum class ProfileLoggedSection {
POSTS,
COMMENTS,
SAVED,
sealed interface ProfileLoggedSection {
object Posts : ProfileLoggedSection
object Comments : ProfileLoggedSection
}

View File

@ -3,8 +3,15 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
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.PostsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
@ -13,8 +20,32 @@ class ProfileLoggedViewModel(
private val mvi: DefaultMviModel<ProfileLoggedMviModel.Intent, ProfileLoggedMviModel.UiState, ProfileLoggedMviModel.Effect>,
private val identityRepository: IdentityRepository,
private val siteRepository: SiteRepository,
private val postsRepository: PostsRepository,
private val commentRepository: CommentRepository,
private val userRepository: UserRepository,
private val notificationCenter: NotificationCenter,
) : ScreenModel,
MviModel<ProfileLoggedMviModel.Intent, ProfileLoggedMviModel.UiState, ProfileLoggedMviModel.Effect> by mvi {
private var currentPage = 1
init {
notificationCenter.addObserver({
(it as? PostModel)?.also { post ->
handlePostUpdate(post)
}
}, this::class.simpleName.orEmpty(), NotificationCenterContractKeys.PostUpdated)
notificationCenter.addObserver({
(it as? PostModel)?.also { post ->
handlePostDelete(post.id)
}
}, this::class.simpleName.orEmpty(), NotificationCenterContractKeys.PostDeleted)
}
fun finalize() {
notificationCenter.removeObserver(this::class.simpleName.orEmpty())
}
override fun onStarted() {
mvi.onStarted()
val auth = identityRepository.authToken.value.orEmpty()
@ -26,9 +57,123 @@ class ProfileLoggedViewModel(
override fun reduce(intent: ProfileLoggedMviModel.Intent) {
when (intent) {
is ProfileLoggedMviModel.Intent.SelectTab -> mvi.updateState {
it.copy(currentTab = intent.value)
is ProfileLoggedMviModel.Intent.ChangeSection -> changeSection(intent.section)
is ProfileLoggedMviModel.Intent.DeleteComment -> deleteComment(intent.id)
is ProfileLoggedMviModel.Intent.DeletePost -> deletePost(intent.id)
ProfileLoggedMviModel.Intent.LoadNextPage -> loadNextPage()
ProfileLoggedMviModel.Intent.Refresh -> refresh()
}
}
private fun refresh() {
currentPage = 1
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
private fun changeSection(section: ProfileLoggedSection) {
currentPage = 1
mvi.updateState {
it.copy(
section = section,
canFetchMore = true,
refreshing = true,
)
}
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
mvi.updateState { it.copy(refreshing = false) }
return
}
mvi.scope?.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val userId = currentState.user?.id ?: 0
val section = currentState.section
if (section == ProfileLoggedSection.Posts) {
val postList = userRepository.getPosts(
auth = auth,
id = userId,
page = currentPage,
sort = SortType.New,
)
val canFetchMore = postList.size >= PostsRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newPosts = if (refreshing) {
postList
} else {
it.posts + postList
}
it.copy(
posts = newPosts,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
} else {
val commentList = userRepository.getComments(
auth = auth,
id = userId,
page = currentPage,
sort = SortType.New,
)
val canFetchMore = commentList.size >= PostsRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newcomments = if (refreshing) {
commentList
} else {
it.comments + commentList
}
it.copy(
comments = newcomments,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
currentPage++
}
}
private fun handlePostUpdate(post: PostModel) {
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
private fun handlePostDelete(id: Int) {
mvi.updateState { it.copy(posts = it.posts.filter { post -> post.id != id }) }
}
private fun deletePost(id: Int) {
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.delete(id = id, auth = auth)
handlePostDelete(id)
}
}
private fun deleteComment(id: Int) {
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.delete(id, auth)
refresh()
}
}
}

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement

View File

@ -1,23 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
interface ProfileCommentsMviModel :
MviModel<ProfileCommentsMviModel.Intent, ProfileCommentsMviModel.UiState, ProfileCommentsMviModel.Effect> {
sealed interface Intent {
object Refresh : Intent
object LoadNextPage : Intent
data class DeleteComment(val id: Int) : Intent
}
data class UiState(
val refreshing: Boolean = false,
val loading: Boolean = false,
val canFetchMore: Boolean = true,
val comments: List<CommentModel> = emptyList(),
)
sealed interface Effect
}

View File

@ -1,149 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments
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.material.ExperimentalMaterialApi
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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.racconforlemmy.core.utils.toLocalPixel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.UserCounters
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.UserHeader
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedSection
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
internal class ProfileCommentsScreen(
private val user: UserModel,
) : Tab {
override val options: TabOptions
@Composable get() {
return TabOptions(1u, "")
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = rememberScreenModel { getProfileCommentsViewModel(user) }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val notificationCenter = remember { getNotificationCenter() }
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(ProfileCommentsMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
UserHeader(user = user)
UserCounters(
modifier = Modifier.graphicsLayer(translationY = -Spacing.m.toLocalPixel()),
user = user,
)
Spacer(modifier = Modifier.height(Spacing.s))
SectionSelector(
titles = listOf(
stringResource(MR.strings.profile_section_posts),
stringResource(MR.strings.profile_section_comments),
stringResource(MR.strings.profile_section_saved),
),
currentSection = 1,
onSectionSelected = {
val section = when (it) {
0 -> ProfileLoggedSection.POSTS
1 -> ProfileLoggedSection.COMMENTS
else -> ProfileLoggedSection.SAVED
}
notificationCenter.getObserver(NotificationCenterContractKeys.SectionChanged)?.also { observer ->
observer.invoke(section)
}
},
)
Spacer(modifier = Modifier.height(Spacing.m))
}
}
items(uiState.comments) { comment ->
ProfileCommentCard(
comment = comment,
options = listOf(stringResource(MR.strings.comment_action_delete)),
onOptionSelected = { idx ->
when (idx) {
else ->
model.reduce(
ProfileCommentsMviModel.Intent.DeleteComment(
comment.id
)
)
}
}
)
}
item {
Spacer(modifier = Modifier.height(Spacing.xxxl))
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(ProfileCommentsMviModel.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.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
)
}
}
}

View File

@ -1,90 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class ProfileCommentsViewModel(
private val mvi: DefaultMviModel<ProfileCommentsMviModel.Intent, ProfileCommentsMviModel.UiState, ProfileCommentsMviModel.Effect>,
private val user: UserModel,
private val identityRepository: IdentityRepository,
private val userRepository: UserRepository,
private val commentRepository: CommentRepository,
) : ScreenModel,
MviModel<ProfileCommentsMviModel.Intent, ProfileCommentsMviModel.UiState, ProfileCommentsMviModel.Effect> by mvi {
private var currentPage: Int = 1
override fun onStarted() {
mvi.onStarted()
if (mvi.uiState.value.comments.isEmpty()) {
refresh()
}
}
override fun reduce(intent: ProfileCommentsMviModel.Intent) {
when (intent) {
ProfileCommentsMviModel.Intent.LoadNextPage -> loadNextPage()
ProfileCommentsMviModel.Intent.Refresh -> refresh()
is ProfileCommentsMviModel.Intent.DeleteComment -> deleteComment(intent.id)
}
}
private fun refresh() {
currentPage = 1
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
mvi.updateState { it.copy(refreshing = false) }
return
}
mvi.scope?.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val commentList = userRepository.getComments(
auth = auth,
id = user.id,
page = currentPage,
sort = SortType.New,
)
currentPage++
val canFetchMore = commentList.size >= PostsRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newcomments = if (refreshing) {
commentList
} else {
it.comments + commentList
}
it.copy(
comments = newcomments,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
}
private fun deleteComment(id: Int) {
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.delete(id, auth)
refresh()
}
}
}

View File

@ -1,23 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
interface ProfilePostsMviModel :
MviModel<ProfilePostsMviModel.Intent, ProfilePostsMviModel.UiState, ProfilePostsMviModel.Effect> {
sealed interface Intent {
object Refresh : Intent
object LoadNextPage : Intent
data class DeletePost(val id: Int) : Intent
}
data class UiState(
val refreshing: Boolean = false,
val loading: Boolean = false,
val canFetchMore: Boolean = true,
val posts: List<PostModel> = emptyList(),
)
sealed interface Effect
}

View File

@ -1,171 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts
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.material.ExperimentalMaterialApi
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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
import com.github.diegoberaldin.racconforlemmy.core.utils.toLocalPixel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.UserCounters
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.UserHeader
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedSection
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
internal class ProfilePostsScreen(
private val user: UserModel,
) : Tab {
override val options: TabOptions
@Composable get() {
return TabOptions(0u, "")
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = rememberScreenModel {
getProfilePostsViewModel(
user = user,
)
}
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
val notificationCenter = remember { getNotificationCenter() }
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(ProfilePostsMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
UserHeader(user = user)
UserCounters(
modifier = Modifier.graphicsLayer(translationY = -Spacing.m.toLocalPixel()),
user = user,
)
Spacer(modifier = Modifier.height(Spacing.s))
SectionSelector(
titles = listOf(
stringResource(MR.strings.profile_section_posts),
stringResource(MR.strings.profile_section_comments),
stringResource(MR.strings.profile_section_saved),
),
currentSection = 0,
onSectionSelected = {
val section = when (it) {
0 -> ProfileLoggedSection.POSTS
1 -> ProfileLoggedSection.COMMENTS
else -> ProfileLoggedSection.SAVED
}
notificationCenter.getObserver(NotificationCenterContractKeys.SectionChanged)
?.also { observer ->
observer.invoke(section)
}
},
)
Spacer(modifier = Modifier.height(Spacing.m))
}
}
items(uiState.posts) { post ->
ProfilePostCard(
modifier = Modifier.onClick {
navigator?.push(
PostDetailScreen(post),
)
},
post = post,
options = listOf(stringResource(MR.strings.comment_action_delete)),
onOpenCommunity = { community ->
navigator?.push(
CommunityDetailScreen(community),
)
},
onImageClick = { url ->
navigator?.push(
ZoomableImageScreen(url),
)
},
onOptionSelected = { idx ->
when (idx) {
else -> model.reduce(
ProfilePostsMviModel.Intent.DeletePost(post.id)
)
}
}
)
}
item {
Spacer(modifier = Modifier.height(Spacing.xxxl))
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(ProfilePostsMviModel.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.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
)
}
}
}

View File

@ -1,131 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
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
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class ProfilePostsViewModel(
private val mvi: DefaultMviModel<ProfilePostsMviModel.Intent, ProfilePostsMviModel.UiState, ProfilePostsMviModel.Effect>,
private val user: UserModel,
private val savedOnly: Boolean = false,
private val identityRepository: IdentityRepository,
private val userRepository: UserRepository,
private val postsRepository: PostsRepository,
private val notificationCenter: NotificationCenter,
) : ScreenModel,
MviModel<ProfilePostsMviModel.Intent, ProfilePostsMviModel.UiState, ProfilePostsMviModel.Effect> by mvi {
private var currentPage: Int = 1
init {
notificationCenter.addObserver({
(it as? PostModel)?.also { post ->
handlePostUpdate(post)
}
}, this::class.simpleName.orEmpty(), NotificationCenterContractKeys.PostUpdated)
notificationCenter.addObserver({
(it as? PostModel)?.also { post ->
handlePostDelete(post.id)
}
}, this::class.simpleName.orEmpty(), NotificationCenterContractKeys.PostDeleted)
}
fun finalize() {
notificationCenter.removeObserver(this::class.simpleName.orEmpty())
}
override fun onStarted() {
mvi.onStarted()
if (mvi.uiState.value.posts.isEmpty()) {
refresh()
}
}
override fun reduce(intent: ProfilePostsMviModel.Intent) {
when (intent) {
ProfilePostsMviModel.Intent.LoadNextPage -> loadNextPage()
ProfilePostsMviModel.Intent.Refresh -> refresh()
is ProfilePostsMviModel.Intent.DeletePost -> deletePost(intent.id)
}
}
private fun refresh() {
currentPage = 1
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
mvi.updateState { it.copy(refreshing = false) }
return
}
mvi.scope?.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val postList = userRepository.getPosts(
auth = auth,
id = user.id,
savedOnly = savedOnly,
page = currentPage,
sort = SortType.New,
)
currentPage++
val canFetchMore = postList.size >= PostsRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newPosts = if (refreshing) {
postList
} else {
it.posts + postList
}
it.copy(
posts = newPosts,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
}
private fun handlePostUpdate(post: PostModel) {
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
private fun handlePostDelete(id: Int) {
mvi.updateState { it.copy(posts = it.posts.filter { post -> post.id != id }) }
}
private fun deletePost(id: Int) {
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.delete(id = id, auth = auth)
handlePostDelete(id)
}
}
}

View File

@ -1,160 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.saved
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.material.ExperimentalMaterialApi
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.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.racconforlemmy.core.utils.toLocalPixel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.UserCounters
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.UserHeader
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedSection
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostCard
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
internal class ProfileSavedScreen(
private val user: UserModel,
) : Tab {
override val options: TabOptions
@Composable get() {
return TabOptions(0u, "")
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = rememberScreenModel {
getProfilePostsViewModel(
user = user,
savedOnly = true,
)
}
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
val notificationCenter = remember { getNotificationCenter() }
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(ProfilePostsMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
UserHeader(user = user)
UserCounters(
modifier = Modifier.graphicsLayer(translationY = -Spacing.m.toLocalPixel()),
user = user,
)
Spacer(modifier = Modifier.height(Spacing.s))
SectionSelector(
titles = listOf(
stringResource(MR.strings.profile_section_posts),
stringResource(MR.strings.profile_section_comments),
stringResource(MR.strings.profile_section_saved),
),
currentSection = 2,
onSectionSelected = {
val section = when (it) {
0 -> ProfileLoggedSection.POSTS
1 -> ProfileLoggedSection.COMMENTS
else -> ProfileLoggedSection.SAVED
}
notificationCenter.getObserver(NotificationCenterContractKeys.SectionChanged)
?.also { observer ->
observer.invoke(section)
}
},
)
Spacer(modifier = Modifier.height(Spacing.m))
}
}
items(uiState.posts) { post ->
ProfilePostCard(
post = post,
onOpenCommunity = { community ->
navigator?.push(
CommunityDetailScreen(community),
)
},
onImageClick = { url ->
navigator?.push(
ZoomableImageScreen(url),
)
},
)
}
item {
Spacer(modifier = Modifier.height(Spacing.xxxl))
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(ProfilePostsMviModel.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.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
)
}
}
}

View File

@ -0,0 +1,33 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved
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
interface ProfileSavedMviModel :
MviModel<ProfileSavedMviModel.Intent, ProfileSavedMviModel.UiState, ProfileSavedMviModel.Effect> {
sealed interface Intent {
object Refresh : Intent
object LoadNextPage : Intent
data class ChangeSection(val section: ProfileSavedSection) : Intent
data class UpVotePost(val index: Int, val feedback: Boolean = false) : Intent
data class DownVotePost(val index: Int, val feedback: Boolean = false) : Intent
data class SavePost(val index: Int, val feedback: Boolean = false) : Intent
data class UpVoteComment(val index: Int, val feedback: Boolean = false) : Intent
data class DownVoteComment(val index: Int, val feedback: Boolean = false) : Intent
data class SaveComment(val index: Int, val feedback: Boolean = false) : Intent
}
data class UiState(
val section: ProfileSavedSection = ProfileSavedSection.Posts,
val refreshing: Boolean = false,
val loading: Boolean = false,
val canFetchMore: Boolean = true,
val blurNsfw: Boolean = true,
val posts: List<PostModel> = emptyList(),
val comments: List<CommentModel> = emptyList(),
)
sealed interface Effect
}

View File

@ -0,0 +1,256 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
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.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CommentCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getProfileSavedViewModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
internal class ProfileSavedScreen(
private val user: UserModel,
) : Tab {
override val options: TabOptions
@Composable get() {
return TabOptions(0u, "")
}
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = rememberScreenModel {
getProfileSavedViewModel(user = user)
}
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
val bottomSheetNavigator = LocalBottomSheetNavigator.current
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
scrollBehavior = scrollBehavior,
title = {
Text(stringResource(MR.strings.profile_section_saved))
},
navigationIcon = {
Image(
modifier = Modifier.onClick {
navigator?.pop()
},
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
)
},
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
SectionSelector(
modifier = Modifier.padding(vertical = Spacing.s),
titles = listOf(
stringResource(MR.strings.profile_section_posts),
stringResource(MR.strings.profile_section_comments),
),
currentSection = when (uiState.section) {
ProfileSavedSection.Comments -> 1
else -> 0
},
onSectionSelected = {
val section = when (it) {
1 -> ProfileSavedSection.Comments
else -> ProfileSavedSection.Posts
}
model.reduce(ProfileSavedMviModel.Intent.ChangeSection(section))
},
)
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(ProfileSavedMviModel.Intent.Refresh)
})
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
if (uiState.section == ProfileSavedSection.Posts) {
itemsIndexed(uiState.posts) { idx, post ->
PostCard(
modifier = Modifier.onClick {
navigator?.push(
PostDetailScreen(post),
)
},
post = post,
blurNsfw = uiState.blurNsfw,
onOpenCommunity = { community ->
navigator?.push(
CommunityDetailScreen(community),
)
},
onOpenCreator = { u ->
if (u.id != user.id) {
navigator?.push(UserDetailScreen(u))
}
},
onUpVote = {
model.reduce(
ProfileSavedMviModel.Intent.UpVotePost(
index = idx,
feedback = true,
),
)
},
onDownVote = {
model.reduce(
ProfileSavedMviModel.Intent.DownVotePost(
index = idx,
feedback = true,
),
)
},
onSave = {
model.reduce(
ProfileSavedMviModel.Intent.SavePost(
index = idx,
feedback = true,
),
)
},
onReply = {
val screen = CreateCommentScreen(
originalPost = post,
)
bottomSheetNavigator.show(screen)
},
onImageClick = { url ->
navigator?.push(
ZoomableImageScreen(url),
)
},
)
}
} else {
itemsIndexed(uiState.comments) { idx, comment ->
CommentCard(
comment = comment,
onUpVote = {
model.reduce(
ProfileSavedMviModel.Intent.UpVoteComment(
index = idx,
feedback = true,
),
)
},
onDownVote = {
model.reduce(
ProfileSavedMviModel.Intent.DownVoteComment(
index = idx,
feedback = true,
),
)
},
onSave = {
model.reduce(
ProfileSavedMviModel.Intent.SaveComment(
index = idx,
feedback = true,
),
)
},
onReply = {
val screen = CreateCommentScreen(
originalPost = PostModel(id = comment.postId),
originalComment = comment,
)
bottomSheetNavigator.show(screen)
},
)
}
}
item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
model.reduce(ProfileSavedMviModel.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.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
}

View File

@ -0,0 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved
sealed interface ProfileSavedSection {
object Posts : ProfileSavedSection
object Comments : ProfileSavedSection
}

View File

@ -0,0 +1,492 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.racconforlemmy.core.utils.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
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.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class ProfileSavedViewModel(
private val mvi: DefaultMviModel<ProfileSavedMviModel.Intent, ProfileSavedMviModel.UiState, ProfileSavedMviModel.Effect>,
private val user: UserModel,
private val identityRepository: IdentityRepository,
private val userRepository: UserRepository,
private val postsRepository: PostsRepository,
private val commentRepository: CommentRepository,
private val notificationCenter: NotificationCenter,
private val hapticFeedback: HapticFeedback,
) : ScreenModel,
MviModel<ProfileSavedMviModel.Intent, ProfileSavedMviModel.UiState, ProfileSavedMviModel.Effect> by mvi {
private var currentPage: Int = 1
init {
notificationCenter.addObserver({
(it as? PostModel)?.also { post ->
handlePostUpdate(post)
}
}, this::class.simpleName.orEmpty(), NotificationCenterContractKeys.PostUpdated)
}
fun finalize() {
notificationCenter.removeObserver(this::class.simpleName.orEmpty())
}
override fun onStarted() {
mvi.onStarted()
if (mvi.uiState.value.posts.isEmpty()) {
refresh()
}
}
override fun reduce(intent: ProfileSavedMviModel.Intent) {
when (intent) {
ProfileSavedMviModel.Intent.LoadNextPage -> loadNextPage()
ProfileSavedMviModel.Intent.Refresh -> refresh()
is ProfileSavedMviModel.Intent.ChangeSection -> changeSection(intent.section)
is ProfileSavedMviModel.Intent.DownVoteComment -> toggleDownVoteComment(
comment = uiState.value.comments[intent.index],
feedback = intent.feedback,
)
is ProfileSavedMviModel.Intent.DownVotePost -> toggleDownVotePost(
post = uiState.value.posts[intent.index],
feedback = intent.feedback,
)
is ProfileSavedMviModel.Intent.SaveComment -> toggleSaveComment(
comment = uiState.value.comments[intent.index],
feedback = intent.feedback,
)
is ProfileSavedMviModel.Intent.SavePost -> toggleSavePost(
post = uiState.value.posts[intent.index],
feedback = intent.feedback,
)
is ProfileSavedMviModel.Intent.UpVoteComment -> toggleUpVoteComment(
comment = uiState.value.comments[intent.index],
feedback = intent.feedback,
)
is ProfileSavedMviModel.Intent.UpVotePost -> toggleUpVotePost(
post = uiState.value.posts[intent.index],
feedback = intent.feedback,
)
}
}
private fun refresh() {
currentPage = 1
mvi.updateState {
it.copy(
canFetchMore = true, refreshing = true
)
}
loadNextPage()
}
private fun loadNextPage() {
val currentState = mvi.uiState.value
if (!currentState.canFetchMore || currentState.loading) {
mvi.updateState { it.copy(refreshing = false) }
return
}
mvi.scope?.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val section = currentState.section
if (section == ProfileSavedSection.Posts) {
val postList = userRepository.getSavedPosts(
auth = auth,
id = user.id,
page = currentPage,
sort = SortType.New,
)
val canFetchMore = postList.size >= PostsRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newPosts = if (refreshing) {
postList
} else {
it.posts + postList
}
it.copy(
posts = newPosts,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
} else {
val commentList = userRepository.getSavedComments(
auth = auth,
id = user.id,
page = currentPage,
sort = SortType.New,
)
val canFetchMore = commentList.size >= PostsRepository.DEFAULT_PAGE_SIZE
mvi.updateState {
val newComments = if (refreshing) {
commentList
} else {
it.comments + commentList
}
it.copy(
comments = newComments,
loading = false,
canFetchMore = canFetchMore,
refreshing = false,
)
}
}
currentPage++
}
}
private fun handlePostUpdate(post: PostModel) {
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
post
} else {
p
}
},
)
}
}
private fun handlePostDelete(id: Int) {
mvi.updateState { it.copy(posts = it.posts.filter { post -> post.id != id }) }
}
private fun deletePost(id: Int) {
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.delete(id = id, auth = auth)
handlePostDelete(id)
}
}
private fun changeSection(section: ProfileSavedSection) {
currentPage = 1
mvi.updateState {
it.copy(
section = section,
canFetchMore = true,
refreshing = true,
)
}
loadNextPage()
}
private fun toggleUpVotePost(
post: PostModel,
feedback: Boolean,
) {
val newValue = post.myVote <= 0
if (feedback) {
hapticFeedback.vibrate()
}
val newPost = postsRepository.asUpVoted(
post = post,
voted = newValue,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.upVote(
auth = auth,
post = post,
voted = newValue,
)
notificationCenter.getObserver(NotificationCenterContractKeys.PostUpdated)?.also {
it.invoke(newPost)
}
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
}
}
}
private fun toggleDownVotePost(
post: PostModel,
feedback: Boolean,
) {
val newValue = post.myVote >= 0
if (feedback) {
hapticFeedback.vibrate()
}
val newPost = postsRepository.asDownVoted(
post = post,
downVoted = newValue,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.downVote(
auth = auth,
post = post,
downVoted = newValue,
)
notificationCenter.getObserver(NotificationCenterContractKeys.PostUpdated)?.also {
it.invoke(newPost)
}
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
}
}
}
private fun toggleSavePost(
post: PostModel,
feedback: Boolean,
) {
val newValue = !post.saved
if (feedback) {
hapticFeedback.vibrate()
}
val newPost = postsRepository.asSaved(
post = post,
saved = newValue,
)
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
postsRepository.save(
auth = auth,
post = post,
saved = newValue,
)
notificationCenter.getObserver(NotificationCenterContractKeys.PostUpdated)?.also {
it.invoke(newPost)
}
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
posts = it.posts.map { p ->
if (p.id == post.id) {
newPost
} else {
p
}
},
)
}
}
}
}
private fun toggleUpVoteComment(
comment: CommentModel,
feedback: Boolean,
) {
val newValue = comment.myVote <= 0
if (feedback) {
hapticFeedback.vibrate()
}
val newComment = commentRepository.asUpVoted(
comment = comment,
voted = newValue,
)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.upVote(
auth = auth,
comment = comment,
voted = newValue,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
comment
} else {
c
}
},
)
}
}
}
}
private fun toggleDownVoteComment(
comment: CommentModel,
feedback: Boolean,
) {
val newValue = comment.myVote >= 0
if (feedback) {
hapticFeedback.vibrate()
}
val newComment = commentRepository.asDownVoted(comment, newValue)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.downVote(
auth = auth,
comment = comment,
downVoted = newValue,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
comment
} else {
c
}
},
)
}
}
}
}
private fun toggleSaveComment(
comment: CommentModel,
feedback: Boolean,
) {
val newValue = !comment.saved
if (feedback) {
hapticFeedback.vibrate()
}
val newComment = commentRepository.asSaved(
comment = comment,
saved = newValue,
)
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
newComment
} else {
c
}
},
)
}
mvi.scope?.launch(Dispatchers.IO) {
try {
val auth = identityRepository.authToken.value.orEmpty()
commentRepository.save(
auth = auth,
comment = comment,
saved = newValue,
)
} catch (e: Throwable) {
e.printStackTrace()
mvi.updateState {
it.copy(
comments = it.comments.map { c ->
if (c.id == comment.id) {
comment
} else {
c
}
},
)
}
}
}
}
}

View File

@ -5,10 +5,8 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileC
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import org.koin.dsl.module
@ -33,26 +31,22 @@ val profileTabModule = module {
mvi = DefaultMviModel(ProfileLoggedMviModel.UiState()),
identityRepository = get(),
siteRepository = get(),
)
}
factory { params ->
ProfilePostsViewModel(
mvi = DefaultMviModel(ProfilePostsMviModel.UiState()),
user = params[0],
savedOnly = params[1],
identityRepository = get(),
userRepository = get(),
postsRepository = get(),
commentRepository = get(),
notificationCenter = get(),
)
}
factory { params ->
ProfileCommentsViewModel(
mvi = DefaultMviModel(ProfileCommentsMviModel.UiState()),
ProfileSavedViewModel(
mvi = DefaultMviModel(ProfileSavedMviModel.UiState()),
user = params[0],
identityRepository = get(),
userRepository = get(),
postsRepository = get(),
commentRepository = get(),
hapticFeedback = get(),
notificationCenter = get(),
)
}
}

View File

@ -3,8 +3,7 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.profile.di
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
expect fun getProfileScreenModel(): ProfileContentViewModel
@ -13,9 +12,4 @@ expect fun getLoginBottomSheetViewModel(): LoginBottomSheetViewModel
expect fun getProfileLoggedViewModel(): ProfileLoggedViewModel
expect fun getProfilePostsViewModel(
user: UserModel,
savedOnly: Boolean = false,
): ProfilePostsViewModel
expect fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewModel
expect fun getProfileSavedViewModel(user: UserModel): ProfileSavedViewModel

View File

@ -3,8 +3,7 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.profile.di
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -17,29 +16,19 @@ actual fun getLoginBottomSheetViewModel() = ProfileScreenModelHelper.loginModel
actual fun getProfileLoggedViewModel(): ProfileLoggedViewModel =
ProfileScreenModelHelper.loggedModel
actual fun getProfilePostsViewModel(
actual fun getProfileSavedViewModel(
user: UserModel,
savedOnly: Boolean,
): ProfilePostsViewModel =
ProfileScreenModelHelper.getPostsModel(user = user, savedOnly = savedOnly)
actual fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewModel =
ProfileScreenModelHelper.getCommentsModel(user)
): ProfileSavedViewModel =
ProfileScreenModelHelper.getSavedModel(user = user)
object ProfileScreenModelHelper : KoinComponent {
val profileModel: ProfileContentViewModel by inject()
val loginModel: LoginBottomSheetViewModel by inject()
val loggedModel: ProfileLoggedViewModel by inject()
fun getPostsModel(user: UserModel, savedOnly: Boolean): ProfilePostsViewModel {
val res: ProfilePostsViewModel by inject(
parameters = { parametersOf(user, savedOnly) },
)
return res
}
fun getCommentsModel(user: UserModel): ProfileCommentsViewModel {
val res: ProfileCommentsViewModel by inject(
fun getSavedModel(user: UserModel): ProfileSavedViewModel {
val res: ProfileSavedViewModel by inject(
parameters = { parametersOf(user) },
)
return res