WIP comentarios

This commit is contained in:
Erik Cavazos 2023-09-02 00:03:57 -06:00
parent bde81ee4e7
commit 4fc72786af
13 changed files with 233 additions and 44 deletions

View File

@ -23,7 +23,7 @@ import com.isolaatti.drafts.ui.DraftsActivity
import com.isolaatti.home.presentation.FeedViewModel
import com.isolaatti.picture_viewer.ui.PictureViewerActivity
import com.isolaatti.posting.PostViewerActivity
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
import com.isolaatti.posting.common.domain.Ownable
import com.isolaatti.posting.common.options_bottom_sheet.domain.OptionClicked

View File

@ -9,7 +9,7 @@ import retrofit2.http.Query
interface CommentsApi {
@POST("Posting/Post/{postId}/Comment")
fun postComment(@Body commentToPost: CommentToPostDto): Call<Nothing>
fun postComment(@Path("postId") postId: Long, @Body commentToPost: CommentToPostDto): Call<CommentDto>
@GET("Fetch/Post/{postId}/Comments")
fun getCommentsOfPosts(@Path("postId") postId: Long, @Query("lastId") lastId: Long, @Query("take") count: Int): Call<FeedCommentsDto>

View File

@ -5,6 +5,7 @@ import com.isolaatti.posting.comments.data.remote.CommentToPostDto
import com.isolaatti.posting.comments.data.remote.CommentsApi
import com.isolaatti.posting.comments.domain.CommentsRepository
import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
@ -26,8 +27,17 @@ class CommentsRepositoryImpl @Inject constructor(private val commentsApi: Commen
}
}
override fun postComment(commentToPostDto: CommentToPostDto, postId: Long): Flow<Boolean> = flow {
val response = commentsApi.postComment(commentToPostDto).awaitResponse()
emit(response.isSuccessful)
override fun postComment(content: String, audioId: String?, postId: Long): Flow<Resource<Comment>> = flow {
emit(Resource.Loading())
val commentToPostDto = CommentToPostDto(content, audioId)
val response = commentsApi.postComment(postId, commentToPostDto).awaitResponse()
if(response.isSuccessful) {
val responseBody = response.body()
if(responseBody != null) {
emit(Resource.Success(Comment.fromCommentDto(responseBody)))
return@flow
}
}
emit(Resource.Error())
}
}

View File

@ -1,12 +1,12 @@
package com.isolaatti.posting.comments.domain
import com.isolaatti.posting.comments.data.remote.CommentDto
import com.isolaatti.posting.comments.data.remote.CommentToPostDto
import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
interface CommentsRepository {
fun getComments(postId: Long, lastId: Long): Flow<MutableList<Comment>>
fun getComment(commentId: Long): Flow<CommentDto>
fun postComment(commentToPostDto: CommentToPostDto, postId: Long): Flow<Boolean>
fun postComment(content: String, audioId: String?, postId: Long): Flow<Resource<Comment>>
}

View File

@ -26,5 +26,16 @@ data class Comment(
)
}
}
fun fromCommentDto(dto: CommentDto): Comment {
return Comment(
id = dto.comment.id,
textContent = dto.comment.textContent,
userId = dto.comment.userId,
postId = dto.comment.postId,
date = dto.comment.date,
username = dto.username
)
}
}
}

View File

@ -0,0 +1,13 @@
package com.isolaatti.posting.comments.domain.use_case
import com.isolaatti.posting.comments.domain.CommentsRepository
import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class PostComment @Inject constructor(private val commentsRepository: CommentsRepository) {
operator fun invoke(content: String, postId: Long): Flow<Resource<Comment>> {
return commentsRepository.postComment(content, null, postId)
}
}

View File

@ -1,5 +1,6 @@
package com.isolaatti.posting.comments.presentation
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
@ -7,12 +8,16 @@ import com.isolaatti.databinding.CommentLayoutBinding
import com.isolaatti.posting.comments.data.remote.CommentDto
import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.posting.common.domain.OnUserInteractedCallback
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.utils.UrlGen
import com.squareup.picasso.Picasso
import io.noties.markwon.Markwon
class CommentsRecyclerViewAdapter(private var list: List<Comment>, private val markwon: Markwon, private val callback: OnUserInteractedCallback) : RecyclerView.Adapter<CommentsRecyclerViewAdapter.CommentViewHolder>() {
private var previousSize = 0
var blockInfiniteScroll = false
inner class CommentViewHolder(val viewBinding: CommentLayoutBinding) : RecyclerView.ViewHolder(viewBinding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder {
@ -21,6 +26,15 @@ class CommentsRecyclerViewAdapter(private var list: List<Comment>, private val m
override fun getItemCount(): Int = list.count()
private var requestedNewContent = false
/**
* Call this method when new content has been added on onLoadMore() callback
*/
fun newContentRequestFinished() {
requestedNewContent = false
}
override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
val comment = list[position]
@ -36,11 +50,53 @@ class CommentsRecyclerViewAdapter(private var list: List<Comment>, private val m
Picasso.get()
.load(UrlGen.userProfileImage(comment.userId))
.into(holder.viewBinding.avatarPicture)
val totalItems = list.size
if(totalItems > 0 && !requestedNewContent) {
if(position == totalItems - 1 && !blockInfiniteScroll) {
requestedNewContent = true
callback.onLoadMore()
}
}
}
fun submitList(commentDtoList: List<Comment>) {
val lastIndex = if(list.count() - 1 < 1) 0 else list.count() - 1
list = commentDtoList
notifyItemRangeChanged(lastIndex, commentDtoList.count())
@SuppressLint("NotifyDataSetChanged")
fun updateList(updatedList: List<Comment>, updateEvent: UpdateEvent? = null) {
if(updateEvent == null) {
list = updatedList
notifyDataSetChanged()
return
}
val commentUpdated = updateEvent.affectedPosition?.let { list?.get(it) }
val position = updateEvent.affectedPosition
previousSize = itemCount
list = updatedList
when(updateEvent.updateType) {
UpdateEvent.UpdateType.COMMENT_REMOVED -> {
if(commentUpdated != null && position != null)
notifyItemRemoved(position)
}
UpdateEvent.UpdateType.COMMENT_ADDED_TOP -> {
notifyItemInserted(0)
}
UpdateEvent.UpdateType.COMMENT_PAGE_ADDED_BOTTOM -> {
notifyItemInserted(previousSize)
}
UpdateEvent.UpdateType.REFRESH -> {
notifyDataSetChanged()
}
}
}
}

View File

@ -7,6 +7,8 @@ import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.comments.data.remote.CommentDto
import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.posting.comments.domain.use_case.GetComments
import com.isolaatti.posting.comments.domain.use_case.PostComment
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ -16,10 +18,14 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class CommentsViewModel @Inject constructor(private val getComments: GetComments) : ViewModel() {
private val _comments: MutableLiveData<List<Comment>> = MutableLiveData()
class CommentsViewModel @Inject constructor(private val getComments: GetComments, private val postComment: PostComment) : ViewModel() {
private val commentsList: MutableList<Comment> = mutableListOf()
val comments: LiveData<List<Comment>> get() = _comments
private val _comments: MutableLiveData<Pair<List<Comment>, UpdateEvent>> = MutableLiveData()
val comments: LiveData<Pair<List<Comment>, UpdateEvent>> get() = _comments
val commentPosted: MutableLiveData<Boolean?> = MutableLiveData()
val noMoreContent: MutableLiveData<Boolean?> = MutableLiveData()
/**
* postId to query comments for. First page will be fetched when set.
@ -33,12 +39,18 @@ class CommentsViewModel @Inject constructor(private val getComments: GetComments
private var lastId: Long = 0L
fun getContent() {
fun getContent(refresh: Boolean = false) {
viewModelScope.launch {
if(refresh) {
commentsList.clear()
}
getComments(postId, lastId).onEach {
val newList = _comments.value?.toMutableList() ?: mutableListOf()
newList.addAll(it)
_comments.postValue(newList)
val eventType = if((commentsList.isNotEmpty())) UpdateEvent.UpdateType.COMMENT_PAGE_ADDED_BOTTOM else UpdateEvent.UpdateType.REFRESH
commentsList.addAll(it)
_comments.postValue(Pair(commentsList, UpdateEvent(eventType, null)))
if(it.isEmpty()) {
noMoreContent.postValue(true)
}
if(it.isNotEmpty()){
lastId = it.last().id
}
@ -47,13 +59,37 @@ class CommentsViewModel @Inject constructor(private val getComments: GetComments
}
}
fun postComment(content: String) {
viewModelScope.launch {
postComment(content, postId).onEach {
when(it) {
is Resource.Success -> {
commentPosted.postValue(true)
putCommentAtTheBeginning(it.data!!)
}
is Resource.Loading -> {
}
is Resource.Error -> {
commentPosted.postValue(false)
}
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}
fun handledCommentPosted() {
commentPosted.postValue(null)
}
/**
* Use when new comment has been posted
*/
fun putCommentAtTheBeginning(commentDto: Comment) {
val newList: MutableList<Comment> = mutableListOf(commentDto)
newList.addAll(_comments.value ?: mutableListOf())
_comments.postValue(newList)
private fun putCommentAtTheBeginning(comment: Comment) {
val newList: MutableList<Comment> = mutableListOf(comment)
newList.addAll(commentsList)
commentsList.clear()
commentsList.addAll(newList)
_comments.postValue(Pair(commentsList, UpdateEvent(UpdateEvent.UpdateType.COMMENT_ADDED_TOP, null)))
}
}

View File

@ -0,0 +1,11 @@
package com.isolaatti.posting.comments.presentation
data class UpdateEvent(val updateType: UpdateType, val affectedPosition: Int?) {
enum class UpdateType {
COMMENT_REMOVED,
COMMENT_ADDED_TOP,
COMMENT_PAGE_ADDED_BOTTOM,
REFRESH
}
}

View File

@ -1,9 +1,11 @@
package com.isolaatti.posting.comments.presentation
package com.isolaatti.posting.comments.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
@ -13,15 +15,16 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.isolaatti.common.Dialogs
import com.isolaatti.databinding.BottomSheetPostCommentsBinding
import com.isolaatti.home.FeedFragment
import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.posting.comments.presentation.CommentsRecyclerViewAdapter
import com.isolaatti.posting.comments.presentation.CommentsViewModel
import com.isolaatti.posting.comments.presentation.UpdateEvent
import com.isolaatti.posting.common.domain.OnUserInteractedCallback
import com.isolaatti.posting.common.domain.Ownable
import com.isolaatti.posting.common.options_bottom_sheet.domain.OptionClicked
import com.isolaatti.posting.common.options_bottom_sheet.domain.Options
import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.profile.ui.ProfileActivity
import com.isolaatti.utils.PicassoImagesPluginDef
import dagger.hilt.android.AndroidEntryPoint
@ -36,12 +39,12 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
private lateinit var viewBinding: BottomSheetPostCommentsBinding
val viewModel: CommentsViewModel by viewModels()
private lateinit var adapter: CommentsRecyclerViewAdapter
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked ->
if(optionClicked?.callerId == BottomSheetPostComments.CALLER_ID) {
if(optionClicked?.callerId == CALLER_ID) {
val comment = optionClicked.payload as? Comment ?: return@Observer
when(optionClicked.optionId) {
Options.Option.OPTION_DELETE -> {
@ -65,6 +68,58 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
}
val commentPostedObserver: Observer<Boolean?> = Observer {
when(it) {
true -> {
clearNewCommentUi()
Toast.makeText(requireContext(), "comment posted", Toast.LENGTH_SHORT).show()
}
false -> {
Toast.makeText(requireContext(), "comment failed to post", Toast.LENGTH_SHORT).show()
}
null -> return@Observer
}
viewModel.handledCommentPosted()
}
private fun setObservers() {
viewModel.comments.observe(viewLifecycleOwner) {
val (list, updateEvent) = it
adapter.updateList(list, updateEvent)
if(updateEvent.updateType == UpdateEvent.UpdateType.COMMENT_ADDED_TOP) {
(viewBinding.recyclerComments.layoutManager as LinearLayoutManager).scrollToPosition(0)
} else {
adapter.newContentRequestFinished()
}
}
viewModel.noMoreContent.observe(viewLifecycleOwner) {
if(it == true) {
adapter.blockInfiniteScroll = true
viewModel.noMoreContent.postValue(null)
}
}
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
viewModel.commentPosted.observe(viewLifecycleOwner, commentPostedObserver)
}
private fun setListeners() {
viewBinding.newCommentTextField.editText?.doOnTextChanged { text, start, before, count ->
viewBinding.submitCommentButton.isEnabled = !text.isNullOrBlank()
}
viewBinding.submitCommentButton.setOnClickListener {
val content = viewBinding.newCommentTextField.editText?.text ?: return@setOnClickListener
viewModel.postComment(content.toString())
}
}
private fun clearNewCommentUi() {
viewBinding.newCommentTextField.editText?.text?.clear()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val postId = arguments?.getLong(ARG_POST_ID)
@ -102,23 +157,16 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
.usePlugin(LinkifyPlugin.create())
.build()
val adapter = CommentsRecyclerViewAdapter(listOf(), markwon, this)
adapter = CommentsRecyclerViewAdapter(listOf(), markwon, this)
viewBinding.recyclerComments.adapter = adapter
viewBinding.recyclerComments.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewModel.comments.observe(viewLifecycleOwner) {
adapter.submitList(it)
}
// New comment area
val textField = viewBinding.newCommentTextField
textField.setStartIconOnClickListener {
}
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
// ensures send button is enabled when there is content on text field,
// even if no change event is triggered
viewBinding.submitCommentButton.isEnabled = !viewBinding.newCommentTextField.editText?.text.isNullOrBlank()
setObservers()
setListeners()
}
companion object {
@ -148,6 +196,6 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
}
override fun onLoadMore() {
TODO("Not yet implemented")
viewModel.getContent()
}
}

View File

@ -19,13 +19,12 @@ import com.isolaatti.databinding.FragmentDiscussionsBinding
import com.isolaatti.followers.domain.FollowingState
import com.isolaatti.home.FeedFragment
import com.isolaatti.posting.PostViewerActivity
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
import com.isolaatti.posting.comments.ui.BottomSheetPostComments
import com.isolaatti.posting.common.domain.Ownable
import com.isolaatti.posting.common.options_bottom_sheet.domain.OptionClicked
import com.isolaatti.posting.common.options_bottom_sheet.domain.Options
import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.presentation.EditPostContract

View File

@ -0,0 +1,4 @@
package com.isolaatti.squads.ui
class SquadsActivity {
}

View File

@ -80,6 +80,7 @@
android:layout_marginTop="4dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp"
android:enabled="false"
app:icon="@drawable/baseline_send_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"