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.home.presentation.FeedViewModel
import com.isolaatti.picture_viewer.ui.PictureViewerActivity import com.isolaatti.picture_viewer.ui.PictureViewerActivity
import com.isolaatti.posting.PostViewerActivity 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.OnUserInteractedWithPostCallback
import com.isolaatti.posting.common.domain.Ownable 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.OptionClicked

View File

@ -9,7 +9,7 @@ import retrofit2.http.Query
interface CommentsApi { interface CommentsApi {
@POST("Posting/Post/{postId}/Comment") @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") @GET("Fetch/Post/{postId}/Comments")
fun getCommentsOfPosts(@Path("postId") postId: Long, @Query("lastId") lastId: Long, @Query("take") count: Int): Call<FeedCommentsDto> 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.data.remote.CommentsApi
import com.isolaatti.posting.comments.domain.CommentsRepository import com.isolaatti.posting.comments.domain.CommentsRepository
import com.isolaatti.posting.comments.domain.model.Comment import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse 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 { override fun postComment(content: String, audioId: String?, postId: Long): Flow<Resource<Comment>> = flow {
val response = commentsApi.postComment(commentToPostDto).awaitResponse() emit(Resource.Loading())
emit(response.isSuccessful) 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 package com.isolaatti.posting.comments.domain
import com.isolaatti.posting.comments.data.remote.CommentDto 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.posting.comments.domain.model.Comment
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface CommentsRepository { interface CommentsRepository {
fun getComments(postId: Long, lastId: Long): Flow<MutableList<Comment>> fun getComments(postId: Long, lastId: Long): Flow<MutableList<Comment>>
fun getComment(commentId: Long): Flow<CommentDto> 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 package com.isolaatti.posting.comments.presentation
import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView 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.data.remote.CommentDto
import com.isolaatti.posting.comments.domain.model.Comment import com.isolaatti.posting.comments.domain.model.Comment
import com.isolaatti.posting.common.domain.OnUserInteractedCallback import com.isolaatti.posting.common.domain.OnUserInteractedCallback
import com.isolaatti.posting.posts.presentation.PostsRecyclerViewAdapter
import com.isolaatti.utils.UrlGen import com.isolaatti.utils.UrlGen
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
class CommentsRecyclerViewAdapter(private var list: List<Comment>, private val markwon: Markwon, private val callback: OnUserInteractedCallback) : RecyclerView.Adapter<CommentsRecyclerViewAdapter.CommentViewHolder>() { 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) inner class CommentViewHolder(val viewBinding: CommentLayoutBinding) : RecyclerView.ViewHolder(viewBinding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CommentViewHolder { 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() 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) { override fun onBindViewHolder(holder: CommentViewHolder, position: Int) {
val comment = list[position] val comment = list[position]
@ -36,11 +50,53 @@ class CommentsRecyclerViewAdapter(private var list: List<Comment>, private val m
Picasso.get() Picasso.get()
.load(UrlGen.userProfileImage(comment.userId)) .load(UrlGen.userProfileImage(comment.userId))
.into(holder.viewBinding.avatarPicture) .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 @SuppressLint("NotifyDataSetChanged")
notifyItemRangeChanged(lastIndex, commentDtoList.count()) 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.data.remote.CommentDto
import com.isolaatti.posting.comments.domain.model.Comment 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.GetComments
import com.isolaatti.posting.comments.domain.use_case.PostComment
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@ -16,10 +18,14 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class CommentsViewModel @Inject constructor(private val getComments: GetComments) : ViewModel() { class CommentsViewModel @Inject constructor(private val getComments: GetComments, private val postComment: PostComment) : ViewModel() {
private val _comments: MutableLiveData<List<Comment>> = MutableLiveData() 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. * 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 private var lastId: Long = 0L
fun getContent() { fun getContent(refresh: Boolean = false) {
viewModelScope.launch { viewModelScope.launch {
if(refresh) {
commentsList.clear()
}
getComments(postId, lastId).onEach { getComments(postId, lastId).onEach {
val newList = _comments.value?.toMutableList() ?: mutableListOf() val eventType = if((commentsList.isNotEmpty())) UpdateEvent.UpdateType.COMMENT_PAGE_ADDED_BOTTOM else UpdateEvent.UpdateType.REFRESH
newList.addAll(it) commentsList.addAll(it)
_comments.postValue(newList) _comments.postValue(Pair(commentsList, UpdateEvent(eventType, null)))
if(it.isEmpty()) {
noMoreContent.postValue(true)
}
if(it.isNotEmpty()){ if(it.isNotEmpty()){
lastId = it.last().id 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 * Use when new comment has been posted
*/ */
fun putCommentAtTheBeginning(commentDto: Comment) { private fun putCommentAtTheBeginning(comment: Comment) {
val newList: MutableList<Comment> = mutableListOf(commentDto) val newList: MutableList<Comment> = mutableListOf(comment)
newList.addAll(_comments.value ?: mutableListOf()) newList.addAll(commentsList)
_comments.postValue(newList) 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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
@ -13,15 +15,16 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.isolaatti.common.Dialogs import com.isolaatti.common.Dialogs
import com.isolaatti.databinding.BottomSheetPostCommentsBinding import com.isolaatti.databinding.BottomSheetPostCommentsBinding
import com.isolaatti.home.FeedFragment
import com.isolaatti.posting.comments.domain.model.Comment 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.OnUserInteractedCallback
import com.isolaatti.posting.common.domain.Ownable 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.OptionClicked
import com.isolaatti.posting.common.options_bottom_sheet.domain.Options 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.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment 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.profile.ui.ProfileActivity
import com.isolaatti.utils.PicassoImagesPluginDef import com.isolaatti.utils.PicassoImagesPluginDef
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -36,12 +39,12 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
private lateinit var viewBinding: BottomSheetPostCommentsBinding private lateinit var viewBinding: BottomSheetPostCommentsBinding
val viewModel: CommentsViewModel by viewModels() val viewModel: CommentsViewModel by viewModels()
private lateinit var adapter: CommentsRecyclerViewAdapter
val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels() val optionsViewModel: BottomSheetPostOptionsViewModel by activityViewModels()
val optionsObserver: Observer<OptionClicked?> = Observer { optionClicked -> 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 val comment = optionClicked.payload as? Comment ?: return@Observer
when(optionClicked.optionId) { when(optionClicked.optionId) {
Options.Option.OPTION_DELETE -> { 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val postId = arguments?.getLong(ARG_POST_ID) val postId = arguments?.getLong(ARG_POST_ID)
@ -102,23 +157,16 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()
val adapter = CommentsRecyclerViewAdapter(listOf(), markwon, this) adapter = CommentsRecyclerViewAdapter(listOf(), markwon, this)
viewBinding.recyclerComments.adapter = adapter viewBinding.recyclerComments.adapter = adapter
viewBinding.recyclerComments.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) viewBinding.recyclerComments.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
viewModel.comments.observe(viewLifecycleOwner) { // ensures send button is enabled when there is content on text field,
adapter.submitList(it) // even if no change event is triggered
} viewBinding.submitCommentButton.isEnabled = !viewBinding.newCommentTextField.editText?.text.isNullOrBlank()
// New comment area
val textField = viewBinding.newCommentTextField
textField.setStartIconOnClickListener {
}
optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver)
setObservers()
setListeners()
} }
companion object { companion object {
@ -148,6 +196,6 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
} }
override fun onLoadMore() { 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.followers.domain.FollowingState
import com.isolaatti.home.FeedFragment import com.isolaatti.home.FeedFragment
import com.isolaatti.posting.PostViewerActivity 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.domain.Ownable
import com.isolaatti.posting.common.options_bottom_sheet.domain.OptionClicked 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.domain.Options
import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel import com.isolaatti.posting.common.options_bottom_sheet.presentation.BottomSheetPostOptionsViewModel
import com.isolaatti.posting.common.options_bottom_sheet.ui.BottomSheetPostOptionsFragment 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.domain.entity.Post
import com.isolaatti.posting.posts.presentation.CreatePostContract import com.isolaatti.posting.posts.presentation.CreatePostContract
import com.isolaatti.posting.posts.presentation.EditPostContract 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_marginTop="4dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="4dp"
android:enabled="false"
app:icon="@drawable/baseline_send_24" app:icon="@drawable/baseline_send_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"