feat(profile): add comment section

This commit is contained in:
Diego Beraldin 2023-08-01 21:51:39 +02:00
parent c3e81fc3ae
commit ec5786c458
12 changed files with 307 additions and 6 deletions

View File

@ -1,5 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_lemmy.data
data class CommentModel(
val id: Int = 0,
val text: String,
val community: CommunityModel? = null,
)

View File

@ -74,7 +74,9 @@ internal fun PostView.toModel() = PostModel(
)
internal fun CommentView.toModel() = CommentModel(
id = comment.id,
text = comment.content,
community = community.toModel(),
)
internal fun Community.toModel() = CommunityModel(

View File

@ -2,6 +2,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.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.login.LoginBottomSheetViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel
@ -30,3 +31,11 @@ actual fun getProfilePostsViewModel(user: UserModel): ProfilePostsViewModel {
)
return res
}
actual fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewModel {
val res: ProfileCommentsViewModel by inject(
clazz = ProfileCommentsViewModel::class.java,
parameters = { parametersOf(user) },
)
return res
}

View File

@ -2,10 +2,8 @@ package com.github.diegoberaldin.raccoonforlemmy.feature_profile.content.logged
import androidx.compose.foundation.layout.Arrangement
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.runtime.Composable
import androidx.compose.runtime.collectAsState
@ -17,6 +15,7 @@ import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.domain_lemmy.data.UserModel
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.di.getProfileLoggedViewModel
@ -36,11 +35,11 @@ internal class ProfileLoggedScreen(
val uiState by model.uiState.collectAsState()
ProfileLoggedHeader(user = user)
ProfileLoggedCounters(user = user)
Spacer(modifier = Modifier.height(Spacing.s))
SectionSelector(
modifier = Modifier.padding(vertical = Spacing.xs),
currentSection = uiState.currentTab,
onSectionSelected = {
model.reduce(ProfileLoggedMviModel.Intent.SelectTab(it))
@ -55,6 +54,10 @@ internal class ProfileLoggedScreen(
}
ProfileLoggedSection.COMMENTS -> {
ProfileCommentsScreen(
modifier = Modifier.weight(1f).fillMaxWidth(),
user = user,
).Content()
}
ProfileLoggedSection.SAVED -> {

View File

@ -24,12 +24,16 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable
internal fun SectionSelector(
modifier: Modifier = Modifier,
currentSection: ProfileLoggedSection,
onSectionSelected: (ProfileLoggedSection) -> Unit,
) {
val highlightColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
Row(
modifier = Modifier.height(25.dp).padding(horizontal = Spacing.m).fillMaxWidth()
modifier = modifier
.height(34.dp)
.padding(horizontal = Spacing.m)
.fillMaxWidth()
.border(
color = highlightColor,
width = 1.dp,
@ -126,4 +130,4 @@ internal fun SectionSelector(
)
}
}
}
}

View File

@ -0,0 +1,81 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_profile.content.logged.comments
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core_md.compose.Markdown
import com.github.diegoberaldin.raccoonforlemmy.domain_lemmy.data.CommentModel
import io.kamel.image.KamelImage
import io.kamel.image.asyncPainterResource
@Composable
fun ProfileCommentCard(
comment: CommentModel,
) {
Card(
modifier = Modifier
.fillMaxWidth()
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(CornerSize.m),
).padding(
vertical = Spacing.lHalf,
horizontal = Spacing.s,
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.xxs),
) {
val communityName = comment.community?.name.orEmpty()
val communityIcon = comment.community?.icon.orEmpty()
val communityHost = comment.community?.host.orEmpty()
val iconSize = 16.dp
if (communityName.isNotEmpty()) {
if (communityIcon.isNotEmpty()) {
val painterResource = asyncPainterResource(data = communityIcon)
KamelImage(
modifier = Modifier.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2)),
resource = painterResource,
contentDescription = null,
contentScale = ContentScale.FillBounds,
)
}
Text(
text = buildString {
append(communityName)
if (communityHost.isNotEmpty()) {
append("@$communityHost")
}
},
style = MaterialTheme.typography.bodySmall,
)
}
}
val body = comment.text
if (body.isNotEmpty()) {
Markdown(content = body)
}
}
}
}

View File

@ -0,0 +1,22 @@
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 UiState(
val refreshing: Boolean = false,
val loading: Boolean = false,
val canFetchMore: Boolean = true,
val comments: List<CommentModel> = emptyList(),
)
sealed interface Effect
}

View File

@ -0,0 +1,81 @@
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.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.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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.domain_lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.di.getProfileCommentsViewModel
internal class ProfileCommentsScreen(
private val modifier: Modifier = Modifier,
private val user: UserModel,
) : Screen {
@OptIn(ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = rememberScreenModel { getProfileCommentsViewModel(user) }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
model.reduce(ProfileCommentsMviModel.Intent.Refresh)
})
Box(
modifier = modifier.pullRefresh(pullRefreshState),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
items(uiState.comments) { comment ->
ProfileCommentCard(comment)
}
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

@ -0,0 +1,73 @@
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.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 ProfileCommentsViewModel(
private val mvi: DefaultMviModel<ProfileCommentsMviModel.Intent, ProfileCommentsMviModel.UiState, ProfileCommentsMviModel.Effect>,
private val user: UserModel,
private val identityRepository: IdentityRepository,
private val userRepository: UserRepository,
) : ScreenModel,
MviModel<ProfileCommentsMviModel.Intent, ProfileCommentsMviModel.UiState, ProfileCommentsMviModel.Effect> by mvi {
private var currentPage: Int = 1
override fun onStarted() {
mvi.onStarted()
refresh()
}
override fun reduce(intent: ProfileCommentsMviModel.Intent) {
when (intent) {
ProfileCommentsMviModel.Intent.LoadNextPage -> loadNextPage()
ProfileCommentsMviModel.Intent.Refresh -> refresh()
}
}
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) {
return
}
mvi.scope.launch(Dispatchers.IO) {
mvi.updateState { it.copy(loading = true) }
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val commentList = userRepository.getUserComments(
auth = auth,
id = user.id,
page = currentPage,
)
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,
)
}
}
}
}

View File

@ -3,6 +3,8 @@ package com.github.diegoberaldin.raccoonforlemmy.feature_profile.di
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
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.login.LoginBottomSheetMviModel
@ -38,4 +40,12 @@ val profileTabModule = module {
userRepository = get(),
)
}
factory { params ->
ProfileCommentsViewModel(
mvi = DefaultMviModel(ProfileCommentsMviModel.UiState()),
user = params[0],
identityRepository = get(),
userRepository = get(),
)
}
}

View File

@ -2,6 +2,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.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.login.LoginBottomSheetViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel
@ -13,3 +14,5 @@ expect fun getLoginBottomSheetViewModel(): LoginBottomSheetViewModel
expect fun getProfileLoggedViewModel(): ProfileLoggedViewModel
expect fun getProfilePostsViewModel(user: UserModel): ProfilePostsViewModel
expect fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewModel

View File

@ -2,6 +2,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.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.login.LoginBottomSheetViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel
@ -19,6 +20,9 @@ actual fun getProfileLoggedViewModel(): ProfileLoggedViewModel =
actual fun getProfilePostsViewModel(user: UserModel): ProfilePostsViewModel =
ProfileScreenModelHelper.getPostsModel(user)
actual fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewModel =
ProfileScreenModelHelper.getCommentsModel(user)
object ProfileScreenModelHelper : KoinComponent {
val profileModel: ProfileScreenModel by inject()
val loginModel: LoginBottomSheetViewModel by inject()
@ -30,4 +34,11 @@ object ProfileScreenModelHelper : KoinComponent {
)
return res
}
fun getCommentsModel(user: UserModel): ProfileCommentsViewModel {
val res: ProfileCommentsViewModel by inject(
parameters = { parametersOf(user) },
)
return res
}
}