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 package com.github.diegoberaldin.raccoonforlemmy.domain_lemmy.data
data class CommentModel( data class CommentModel(
val id: Int = 0,
val text: String, val text: String,
val community: CommunityModel? = null,
) )

View File

@ -74,7 +74,9 @@ internal fun PostView.toModel() = PostModel(
) )
internal fun CommentView.toModel() = CommentModel( internal fun CommentView.toModel() = CommentModel(
id = comment.id,
text = comment.content, text = comment.content,
community = community.toModel(),
) )
internal fun Community.toModel() = CommunityModel( 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.domain_lemmy.data.UserModel
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.logged.posts.ProfilePostsViewModel 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 com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel import com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel
@ -30,3 +31,11 @@ actual fun getProfilePostsViewModel(user: UserModel): ProfilePostsViewModel {
) )
return res 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.Arrangement
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState 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_appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle import com.github.diegoberaldin.raccoonforlemmy.core_architecture.bindToLifecycle
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.logged.comments.ProfileCommentsScreen
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.content.logged.posts.ProfilePostsScreen import com.github.diegoberaldin.raccoonforlemmy.feature_profile.content.logged.posts.ProfilePostsScreen
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.di.getProfileLoggedViewModel import com.github.diegoberaldin.raccoonforlemmy.feature_profile.di.getProfileLoggedViewModel
@ -36,11 +35,11 @@ internal class ProfileLoggedScreen(
val uiState by model.uiState.collectAsState() val uiState by model.uiState.collectAsState()
ProfileLoggedHeader(user = user) ProfileLoggedHeader(user = user)
ProfileLoggedCounters(user = user) ProfileLoggedCounters(user = user)
Spacer(modifier = Modifier.height(Spacing.s))
SectionSelector( SectionSelector(
modifier = Modifier.padding(vertical = Spacing.xs),
currentSection = uiState.currentTab, currentSection = uiState.currentTab,
onSectionSelected = { onSectionSelected = {
model.reduce(ProfileLoggedMviModel.Intent.SelectTab(it)) model.reduce(ProfileLoggedMviModel.Intent.SelectTab(it))
@ -55,6 +54,10 @@ internal class ProfileLoggedScreen(
} }
ProfileLoggedSection.COMMENTS -> { ProfileLoggedSection.COMMENTS -> {
ProfileCommentsScreen(
modifier = Modifier.weight(1f).fillMaxWidth(),
user = user,
).Content()
} }
ProfileLoggedSection.SAVED -> { ProfileLoggedSection.SAVED -> {

View File

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

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.core_architecture.DefaultMviModel
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.logged.comments.ProfileCommentsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.content.logged.posts.ProfilePostsMviModel import com.github.diegoberaldin.raccoonforlemmy.feature_profile.content.logged.posts.ProfilePostsMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.content.logged.posts.ProfilePostsViewModel import com.github.diegoberaldin.raccoonforlemmy.feature_profile.content.logged.posts.ProfilePostsViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.login.LoginBottomSheetMviModel import com.github.diegoberaldin.raccoonforlemmy.feature_profile.login.LoginBottomSheetMviModel
@ -38,4 +40,12 @@ val profileTabModule = module {
userRepository = get(), 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.domain_lemmy.data.UserModel
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.logged.posts.ProfilePostsViewModel 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 com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel import com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel
@ -13,3 +14,5 @@ expect fun getLoginBottomSheetViewModel(): LoginBottomSheetViewModel
expect fun getProfileLoggedViewModel(): ProfileLoggedViewModel expect fun getProfileLoggedViewModel(): ProfileLoggedViewModel
expect fun getProfilePostsViewModel(user: UserModel): ProfilePostsViewModel 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.domain_lemmy.data.UserModel
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.logged.posts.ProfilePostsViewModel 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 com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel import com.github.diegoberaldin.raccoonforlemmy.feature_profile.viewmodel.ProfileScreenModel
@ -19,6 +20,9 @@ actual fun getProfileLoggedViewModel(): ProfileLoggedViewModel =
actual fun getProfilePostsViewModel(user: UserModel): ProfilePostsViewModel = actual fun getProfilePostsViewModel(user: UserModel): ProfilePostsViewModel =
ProfileScreenModelHelper.getPostsModel(user) ProfileScreenModelHelper.getPostsModel(user)
actual fun getProfileCommentsViewModel(user: UserModel): ProfileCommentsViewModel =
ProfileScreenModelHelper.getCommentsModel(user)
object ProfileScreenModelHelper : KoinComponent { object ProfileScreenModelHelper : KoinComponent {
val profileModel: ProfileScreenModel by inject() val profileModel: ProfileScreenModel by inject()
val loginModel: LoginBottomSheetViewModel by inject() val loginModel: LoginBottomSheetViewModel by inject()
@ -30,4 +34,11 @@ object ProfileScreenModelHelper : KoinComponent {
) )
return res return res
} }
fun getCommentsModel(user: UserModel): ProfileCommentsViewModel {
val res: ProfileCommentsViewModel by inject(
parameters = { parametersOf(user) },
)
return res
}
} }