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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape 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.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp 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.racconforlemmy.core.utils.toLocalPixel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
@ -27,6 +31,7 @@ import io.kamel.image.asyncPainterResource
@Composable @Composable
fun UserHeader( fun UserHeader(
user: UserModel, user: UserModel,
onOpenBookmarks: (() -> Unit)? = null,
) { ) {
val userAvatar = user.avatar.orEmpty() val userAvatar = user.avatar.orEmpty()
val userDisplayName = user.name 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, page: Int,
limit: Int = PostsRepository.DEFAULT_PAGE_SIZE, limit: Int = PostsRepository.DEFAULT_PAGE_SIZE,
sort: SortType = SortType.Active, sort: SortType = SortType.Active,
savedOnly: Boolean = false,
): List<PostModel> = runCatching { ): List<PostModel> = runCatching {
val response = serviceProvider.user.getDetails( val response = serviceProvider.user.getDetails(
auth = auth, auth = auth,
@ -51,7 +50,25 @@ class UserRepository(
page = page, page = page,
limit = limit, limit = limit,
sort = sort.toCommentDto(), 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() val dto = response.body() ?: return@runCatching emptyList()
dto.posts.map { it.toModel() } dto.posts.map { it.toModel() }
@ -75,6 +92,25 @@ class UserRepository(
dto.comments.map { it.toModel() } dto.comments.map { it.toModel() }
}.getOrElse { emptyList() } }.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( suspend fun getMentions(
auth: String? = null, auth: String? = null,
page: Int, 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.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel 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.ProfileLoggedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
@ -24,20 +23,9 @@ actual fun getProfileLoggedViewModel(): ProfileLoggedViewModel {
return res return res
} }
actual fun getProfilePostsViewModel( actual fun getProfileSavedViewModel(user: UserModel): ProfileSavedViewModel {
user: UserModel, val res: ProfileSavedViewModel by inject(
savedOnly: Boolean, clazz = ProfileSavedViewModel::class.java,
): 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,
parameters = { parametersOf(user) }, parameters = { parametersOf(user) },
) )
return res 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.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement

View File

@ -1,18 +1,29 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel 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 import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
interface ProfileLoggedMviModel : interface ProfileLoggedMviModel :
MviModel<ProfileLoggedMviModel.Intent, ProfileLoggedMviModel.UiState, ProfileLoggedMviModel.Effect> { MviModel<ProfileLoggedMviModel.Intent, ProfileLoggedMviModel.UiState, ProfileLoggedMviModel.Effect> {
sealed interface Intent { 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( data class UiState(
val user: UserModel? = null, 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 sealed interface Effect

View File

@ -1,34 +1,50 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.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.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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.Tab
import cafe.adriel.voyager.navigator.tab.TabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions 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.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle 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.core.notifications.di.getNotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsScreen import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedScreen
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.di.getProfileLoggedViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.di.getProfileLoggedViewModel
import kotlinx.coroutines.flow.launchIn import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import kotlinx.coroutines.flow.map import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.onEach
internal object ProfileLoggedScreen : Tab { internal object ProfileLoggedScreen : Tab {
@ -37,6 +53,7 @@ internal object ProfileLoggedScreen : Tab {
return TabOptions(0u, "") return TabOptions(0u, "")
} }
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
override fun Content() { override fun Content() {
Column( Column(
@ -49,6 +66,7 @@ internal object ProfileLoggedScreen : Tab {
val uiState by model.uiState.collectAsState() val uiState by model.uiState.collectAsState()
val user = uiState.user val user = uiState.user
val notificationCenter = remember { getNotificationCenter() } val notificationCenter = remember { getNotificationCenter() }
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
DisposableEffect(key) { DisposableEffect(key) {
onDispose { onDispose {
notificationCenter.removeObserver(key) notificationCenter.removeObserver(key)
@ -56,47 +74,133 @@ internal object ProfileLoggedScreen : Tab {
} }
if (user != null) { if (user != null) {
val screens = remember { val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
val postsScreen = ProfilePostsScreen(user) model.reduce(ProfileLoggedMviModel.Intent.Refresh)
val commentsScreen = ProfileCommentsScreen(user) })
val savedScreen = ProfileSavedScreen(user) Box(
modifier = Modifier.pullRefresh(pullRefreshState),
notificationCenter.addObserver({ ) {
(it as? ProfileLoggedSection)?.also { value -> LazyColumn(
model.reduce(ProfileLoggedMviModel.Intent.SelectTab(value)) 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) if (uiState.section == ProfileLoggedSection.Posts) {
notificationCenter.addObserver({ items(uiState.posts) { post ->
(it as? ProfileLoggedSection)?.also { value -> ProfilePostCard(
model.reduce(ProfileLoggedMviModel.Intent.SelectTab(value)) 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) item {
notificationCenter.addObserver({ Spacer(modifier = Modifier.height(Spacing.xxxl))
(it as? ProfileLoggedSection)?.also { value ->
model.reduce(ProfileLoggedMviModel.Intent.SelectTab(value))
} }
}, key, NotificationCenterContractKeys.SectionChanged) item {
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
listOf( model.reduce(ProfileLoggedMviModel.Intent.LoadNextPage)
postsScreen, }
commentsScreen, if (uiState.loading && !uiState.refreshing) {
savedScreen, Box(
) modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
} contentAlignment = Alignment.Center,
TabNavigator(screens.first()) { ) {
CurrentScreen() CircularProgressIndicator(
val navigator = LocalTabNavigator.current modifier = Modifier.size(25.dp),
LaunchedEffect(model) { color = MaterialTheme.colorScheme.primary,
model.uiState.map { it.currentTab } )
.onEach { section ->
val index = when (section) {
ProfileLoggedSection.POSTS -> 0
ProfileLoggedSection.COMMENTS -> 1
ProfileLoggedSection.SAVED -> 2
} }
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 package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
enum class ProfileLoggedSection { sealed interface ProfileLoggedSection {
POSTS, object Posts : ProfileLoggedSection
COMMENTS, object Comments : ProfileLoggedSection
SAVED,
} }

View File

@ -3,8 +3,15 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel 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.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.SiteRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -13,8 +20,32 @@ class ProfileLoggedViewModel(
private val mvi: DefaultMviModel<ProfileLoggedMviModel.Intent, ProfileLoggedMviModel.UiState, ProfileLoggedMviModel.Effect>, private val mvi: DefaultMviModel<ProfileLoggedMviModel.Intent, ProfileLoggedMviModel.UiState, ProfileLoggedMviModel.Effect>,
private val identityRepository: IdentityRepository, private val identityRepository: IdentityRepository,
private val siteRepository: SiteRepository, private val siteRepository: SiteRepository,
private val postsRepository: PostsRepository,
private val commentRepository: CommentRepository,
private val userRepository: UserRepository,
private val notificationCenter: NotificationCenter,
) : ScreenModel, ) : ScreenModel,
MviModel<ProfileLoggedMviModel.Intent, ProfileLoggedMviModel.UiState, ProfileLoggedMviModel.Effect> by mvi { 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() { override fun onStarted() {
mvi.onStarted() mvi.onStarted()
val auth = identityRepository.authToken.value.orEmpty() val auth = identityRepository.authToken.value.orEmpty()
@ -26,9 +57,123 @@ class ProfileLoggedViewModel(
override fun reduce(intent: ProfileLoggedMviModel.Intent) { override fun reduce(intent: ProfileLoggedMviModel.Intent) {
when (intent) { when (intent) {
is ProfileLoggedMviModel.Intent.SelectTab -> mvi.updateState { is ProfileLoggedMviModel.Intent.ChangeSection -> changeSection(intent.section)
it.copy(currentTab = intent.value) 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.background
import androidx.compose.foundation.layout.Arrangement 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.ProfileContentViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.ProfileLoggedMviModel 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.ProfileLoggedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsMviModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedViewModel
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.login.LoginBottomSheetMviModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
import org.koin.dsl.module import org.koin.dsl.module
@ -33,26 +31,22 @@ val profileTabModule = module {
mvi = DefaultMviModel(ProfileLoggedMviModel.UiState()), mvi = DefaultMviModel(ProfileLoggedMviModel.UiState()),
identityRepository = get(), identityRepository = get(),
siteRepository = get(), siteRepository = get(),
)
}
factory { params ->
ProfilePostsViewModel(
mvi = DefaultMviModel(ProfilePostsMviModel.UiState()),
user = params[0],
savedOnly = params[1],
identityRepository = get(),
userRepository = get(), userRepository = get(),
postsRepository = get(), postsRepository = get(),
commentRepository = get(),
notificationCenter = get(), notificationCenter = get(),
) )
} }
factory { params -> factory { params ->
ProfileCommentsViewModel( ProfileSavedViewModel(
mvi = DefaultMviModel(ProfileCommentsMviModel.UiState()), mvi = DefaultMviModel(ProfileSavedMviModel.UiState()),
user = params[0], user = params[0],
identityRepository = get(), identityRepository = get(),
userRepository = get(), userRepository = get(),
postsRepository = get(),
commentRepository = 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.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.ProfileContentViewModel 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.ProfileLoggedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.comments.ProfileCommentsViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.saved.ProfileSavedViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.profile.login.LoginBottomSheetViewModel
expect fun getProfileScreenModel(): ProfileContentViewModel expect fun getProfileScreenModel(): ProfileContentViewModel
@ -13,9 +12,4 @@ expect fun getLoginBottomSheetViewModel(): LoginBottomSheetViewModel
expect fun getProfileLoggedViewModel(): ProfileLoggedViewModel expect fun getProfileLoggedViewModel(): ProfileLoggedViewModel
expect fun getProfilePostsViewModel( expect fun getProfileSavedViewModel(user: UserModel): ProfileSavedViewModel
user: UserModel,
savedOnly: Boolean = false,
): ProfilePostsViewModel
expect fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewModel

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