From 3c146ee60379e69994a3433a68728cf2e9a0431c Mon Sep 17 00:00:00 2001 From: Erik Cavazos Date: Sat, 2 Sep 2023 23:33:32 -0600 Subject: [PATCH] WIP --- .../comments/data/remote/CommentsApi.kt | 7 ++ .../data/repository/CommentsRepositoryImpl.kt | 28 +++++ .../comments/domain/CommentsRepository.kt | 2 + .../comments/domain/use_case/DeleteComment.kt | 12 ++ .../comments/domain/use_case/EditComment.kt | 13 +++ .../CommentsRecyclerViewAdapter.kt | 8 +- .../presentation/CommentsViewModel.kt | 88 ++++++++++++-- .../comments/presentation/UpdateEvent.kt | 1 + .../comments/ui/BottomSheetPostComments.kt | 21 ++-- .../comments/ui/EditCommentDialogFragment.kt | 107 ++++++++++++++++++ .../main/res/layout-land/fragment_feed.xml | 2 +- .../main/res/layout/fragment_edit_comment.xml | 78 +++++++++++++ app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/themes.xml | 7 ++ 14 files changed, 350 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/com/isolaatti/posting/comments/domain/use_case/DeleteComment.kt create mode 100644 app/src/main/java/com/isolaatti/posting/comments/domain/use_case/EditComment.kt create mode 100644 app/src/main/java/com/isolaatti/posting/comments/ui/EditCommentDialogFragment.kt create mode 100644 app/src/main/res/layout/fragment_edit_comment.xml diff --git a/app/src/main/java/com/isolaatti/posting/comments/data/remote/CommentsApi.kt b/app/src/main/java/com/isolaatti/posting/comments/data/remote/CommentsApi.kt index c02716d..c055644 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/data/remote/CommentsApi.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/data/remote/CommentsApi.kt @@ -1,5 +1,6 @@ package com.isolaatti.posting.comments.data.remote +import com.isolaatti.utils.LongIdentificationWrapper import retrofit2.Call import retrofit2.http.Body import retrofit2.http.GET @@ -16,4 +17,10 @@ interface CommentsApi { @GET("Fetch/Comments/{commentId}") fun getComment(@Path("commentId") commentId: Long): Call + + @POST("Comment/{commentId}/Edit") + fun editComment(@Path("commentId") commentId: Long, @Body commentDto: CommentToPostDto): Call + + @POST("Comment/Delete") + fun deleteComment(@Body commentId: LongIdentificationWrapper): Call } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/data/repository/CommentsRepositoryImpl.kt b/app/src/main/java/com/isolaatti/posting/comments/data/repository/CommentsRepositoryImpl.kt index acba667..c748d4b 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/data/repository/CommentsRepositoryImpl.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/data/repository/CommentsRepositoryImpl.kt @@ -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.LongIdentificationWrapper import com.isolaatti.utils.Resource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -40,4 +41,31 @@ class CommentsRepositoryImpl @Inject constructor(private val commentsApi: Commen } emit(Resource.Error()) } + + override fun editComment(commentId: Long, content: String, audioId: String?): Flow> = flow { + emit(Resource.Loading()) + val commentToPostDto = CommentToPostDto(content, audioId) + val response = commentsApi.editComment(commentId, commentToPostDto).awaitResponse() + + if(response.isSuccessful) { + val responseBody = response.body() + if(responseBody != null) { + emit(Resource.Success(Comment.fromCommentDto(responseBody))) + return@flow + } + } + emit(Resource.Error()) + + } + + override fun deleteComment(commentId: Long): Flow> = flow { + emit(Resource.Loading()) + val response = commentsApi.deleteComment(LongIdentificationWrapper(commentId)).awaitResponse() + + if(response.isSuccessful) { + emit(Resource.Success(true)) + return@flow + } + emit(Resource.Error()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/domain/CommentsRepository.kt b/app/src/main/java/com/isolaatti/posting/comments/domain/CommentsRepository.kt index 11f082f..8f60f0d 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/domain/CommentsRepository.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/domain/CommentsRepository.kt @@ -9,4 +9,6 @@ interface CommentsRepository { fun getComments(postId: Long, lastId: Long): Flow> fun getComment(commentId: Long): Flow fun postComment(content: String, audioId: String?, postId: Long): Flow> + fun editComment(commentId: Long, content: String, audioId: String?): Flow> + fun deleteComment(commentId: Long): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/DeleteComment.kt b/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/DeleteComment.kt new file mode 100644 index 0000000..a84c83f --- /dev/null +++ b/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/DeleteComment.kt @@ -0,0 +1,12 @@ +package com.isolaatti.posting.comments.domain.use_case + +import com.isolaatti.posting.comments.domain.CommentsRepository +import com.isolaatti.utils.Resource +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class DeleteComment @Inject constructor(private val commentsRepository: CommentsRepository) { + operator fun invoke(commentId: Long): Flow> { + return commentsRepository.deleteComment(commentId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/EditComment.kt b/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/EditComment.kt new file mode 100644 index 0000000..64d9b77 --- /dev/null +++ b/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/EditComment.kt @@ -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 EditComment @Inject constructor(private val commentsRepository: CommentsRepository) { + operator fun invoke(commentId: Long, content: String, audioId: String?): Flow> { + return commentsRepository.editComment(commentId,content,audioId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/presentation/CommentsRecyclerViewAdapter.kt b/app/src/main/java/com/isolaatti/posting/comments/presentation/CommentsRecyclerViewAdapter.kt index 940beff..33c25ad 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/presentation/CommentsRecyclerViewAdapter.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/presentation/CommentsRecyclerViewAdapter.kt @@ -73,7 +73,7 @@ class CommentsRecyclerViewAdapter(private var list: List, private val m return } - val commentUpdated = updateEvent.affectedPosition?.let { list?.get(it) } + val commentUpdated = updateEvent.affectedPosition?.let { list[it] } val position = updateEvent.affectedPosition previousSize = itemCount @@ -97,6 +97,12 @@ class CommentsRecyclerViewAdapter(private var list: List, private val m UpdateEvent.UpdateType.REFRESH -> { notifyDataSetChanged() } + + UpdateEvent.UpdateType.COMMENT_UPDATED -> { + if(updateEvent.affectedPosition != null) { + notifyItemChanged(updateEvent.affectedPosition) + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/presentation/CommentsViewModel.kt b/app/src/main/java/com/isolaatti/posting/comments/presentation/CommentsViewModel.kt index db38cb4..0728c01 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/presentation/CommentsViewModel.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/presentation/CommentsViewModel.kt @@ -4,8 +4,9 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel 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.DeleteComment +import com.isolaatti.posting.comments.domain.use_case.EditComment import com.isolaatti.posting.comments.domain.use_case.GetComments import com.isolaatti.posting.comments.domain.use_case.PostComment import com.isolaatti.utils.Resource @@ -18,7 +19,12 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class CommentsViewModel @Inject constructor(private val getComments: GetComments, private val postComment: PostComment) : ViewModel() { +class CommentsViewModel @Inject constructor( + private val getComments: GetComments, + private val postComment: PostComment, + private val editCommentUseCase: EditComment, + private val deleteCommentUseCase: DeleteComment +) : ViewModel() { private val commentsList: MutableList = mutableListOf() private val _comments: MutableLiveData, UpdateEvent>> = MutableLiveData() @@ -26,34 +32,36 @@ class CommentsViewModel @Inject constructor(private val getComments: GetComments val comments: LiveData, UpdateEvent>> get() = _comments val commentPosted: MutableLiveData = MutableLiveData() val noMoreContent: MutableLiveData = MutableLiveData() - val commentToEdit: MutableLiveData = MutableLiveData() + val commentToEdit: MutableLiveData = MutableLiveData() val finishedEditingComment: MutableLiveData = MutableLiveData() + val error: MutableLiveData = MutableLiveData(false) /** * postId to query comments for. First page will be fetched when set. * Call getContent() to get more content if available - */ + */ var postId: Long = 0 set(value) { field = value getContent() - } + } private var lastId: Long = 0L fun getContent(refresh: Boolean = false) { viewModelScope.launch { - if(refresh) { + if (refresh) { commentsList.clear() } getComments(postId, lastId).onEach { - val eventType = if((commentsList.isNotEmpty())) UpdateEvent.UpdateType.COMMENT_PAGE_ADDED_BOTTOM else UpdateEvent.UpdateType.REFRESH + 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()) { + if (it.isEmpty()) { noMoreContent.postValue(true) } - if(it.isNotEmpty()){ + if (it.isNotEmpty()) { lastId = it.last().id } @@ -64,16 +72,69 @@ class CommentsViewModel @Inject constructor(private val getComments: GetComments fun postComment(content: String) { viewModelScope.launch { postComment(content, postId).onEach { - when(it) { + when (it) { is Resource.Success -> { commentPosted.postValue(true) putCommentAtTheBeginning(it.data!!) } + is Resource.Loading -> { } + is Resource.Error -> { commentPosted.postValue(false) + error.postValue(true) + } + } + }.flowOn(Dispatchers.IO).launchIn(this) + } + } + + fun editComment(newContent: String) { + // TODO handle audios + val comment = commentToEdit.value ?: return + viewModelScope.launch { + commentToEdit.postValue(null) + editCommentUseCase(comment.id, newContent, null).onEach { commentResource -> + when (commentResource) { + is Resource.Success -> { + val newComment = commentResource.data ?: return@onEach + val oldComment = + commentsList.find { it.id == newComment.id } ?: return@onEach + + val index = commentsList.indexOf(oldComment) + commentsList[index] = newComment + val updateEvent = UpdateEvent(UpdateEvent.UpdateType.COMMENT_UPDATED, index) + _comments.postValue(Pair(commentsList, updateEvent)) + finishedEditingComment.postValue(true) + } + + is Resource.Error -> { + error.postValue(true) + } + + is Resource.Loading -> {} + } + + }.flowOn(Dispatchers.IO).launchIn(this) + } + + } + + fun deleteComment(commentId: Long) { + val index = + commentsList.find { it.id == commentId }?.let { commentsList.indexOf(it) } ?: return + viewModelScope.launch { + deleteCommentUseCase(commentId).onEach { + when(it) { + is Resource.Error -> { + error.postValue(true) + } + is Resource.Loading -> {} + is Resource.Success -> { + commentsList.removeAt(index) + _comments.postValue(Pair(commentsList, UpdateEvent(UpdateEvent.UpdateType.COMMENT_REMOVED, index))) } } }.flowOn(Dispatchers.IO).launchIn(this) @@ -92,7 +153,12 @@ class CommentsViewModel @Inject constructor(private val getComments: GetComments newList.addAll(commentsList) commentsList.clear() commentsList.addAll(newList) - _comments.postValue(Pair(commentsList, UpdateEvent(UpdateEvent.UpdateType.COMMENT_ADDED_TOP, null))) + _comments.postValue( + Pair( + commentsList, + UpdateEvent(UpdateEvent.UpdateType.COMMENT_ADDED_TOP, null) + ) + ) } fun switchToEditMode(comment: Comment) { diff --git a/app/src/main/java/com/isolaatti/posting/comments/presentation/UpdateEvent.kt b/app/src/main/java/com/isolaatti/posting/comments/presentation/UpdateEvent.kt index ce20179..6ffcd71 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/presentation/UpdateEvent.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/presentation/UpdateEvent.kt @@ -6,6 +6,7 @@ data class UpdateEvent(val updateType: UpdateType, val affectedPosition: Int?) { COMMENT_REMOVED, COMMENT_ADDED_TOP, COMMENT_PAGE_ADDED_BOTTOM, + COMMENT_UPDATED, REFRESH } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/ui/BottomSheetPostComments.kt b/app/src/main/java/com/isolaatti/posting/comments/ui/BottomSheetPostComments.kt index 2c92d1e..8a1d78d 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/ui/BottomSheetPostComments.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/ui/BottomSheetPostComments.kt @@ -56,7 +56,7 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC Dialogs.buildDeleteCommentDialog(requireContext()) { delete -> optionsViewModel.handle() if(delete) { - // remove comment + viewModel.deleteComment(comment.id) } }.show() @@ -106,25 +106,22 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC } } - private val finishedEditingComment: Observer = Observer { - if(it == true) { - switchEditionModeUi(false) - } + private val commentToEditObserver: Observer = Observer { + EditCommentDialogFragment().show(childFragmentManager, EditCommentDialogFragment.TAG) } - private val commentToEditObserver: Observer = Observer { - switchEditionModeUi(true) - - viewBinding.newCommentTextField.editText?.setText(it.textContent) - } - private fun setObservers() { viewModel.comments.observe(viewLifecycleOwner, commentsObserver) viewModel.noMoreContent.observe(viewLifecycleOwner, noMoreContentObserver) viewModel.commentPosted.observe(viewLifecycleOwner, commentPostedObserver) viewModel.commentToEdit.observe(viewLifecycleOwner, commentToEditObserver) - viewModel.finishedEditingComment.observe(viewLifecycleOwner, finishedEditingComment) + viewModel.error.observe(viewLifecycleOwner) { + if(it == true) { + Toast.makeText(requireContext(), "error", Toast.LENGTH_SHORT).show() + viewModel.error.postValue(null) + } + } optionsViewModel.optionClicked.observe(viewLifecycleOwner, optionsObserver) } diff --git a/app/src/main/java/com/isolaatti/posting/comments/ui/EditCommentDialogFragment.kt b/app/src/main/java/com/isolaatti/posting/comments/ui/EditCommentDialogFragment.kt new file mode 100644 index 0000000..601ba85 --- /dev/null +++ b/app/src/main/java/com/isolaatti/posting/comments/ui/EditCommentDialogFragment.kt @@ -0,0 +1,107 @@ +package com.isolaatti.posting.comments.ui + +import android.app.Dialog +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.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import com.isolaatti.R +import com.isolaatti.databinding.FragmentEditCommentBinding +import com.isolaatti.posting.comments.presentation.CommentsViewModel +import com.isolaatti.utils.PicassoImagesPluginDef +import com.isolaatti.utils.UrlGen +import com.squareup.picasso.Picasso +import io.noties.markwon.AbstractMarkwonPlugin +import io.noties.markwon.Markwon +import io.noties.markwon.MarkwonConfiguration +import io.noties.markwon.image.destination.ImageDestinationProcessorRelativeToAbsolute +import io.noties.markwon.linkify.LinkifyPlugin + +class EditCommentDialogFragment : DialogFragment() { + private val viewModel: CommentsViewModel by viewModels({requireParentFragment()}) + private lateinit var binding: FragmentEditCommentBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setStyle(STYLE_NORMAL, R.style.AlertDialogTransparent) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentEditCommentBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val markwon = Markwon.builder(requireContext()) + .usePlugin(object: AbstractMarkwonPlugin() { + override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { + builder + .imageDestinationProcessor( + ImageDestinationProcessorRelativeToAbsolute + .create("https://isolaatti.com/")) + } + }) + .usePlugin(PicassoImagesPluginDef.picassoImagePlugin) + .usePlugin(LinkifyPlugin.create()) + .build() + + binding.comment.moreButton.visibility = View.GONE + binding.newCommentTextField.editText?.doOnTextChanged { text, start, before, count -> + binding.submitCommentButton.isEnabled = !text.isNullOrBlank() + } + + binding.submitCommentButton.setOnClickListener { + val newContent = binding.newCommentTextField.editText?.text + if(newContent != null) { + binding.submitCommentButton.isEnabled = false + viewModel.editComment(newContent.toString()) + } + } + + viewModel.commentToEdit.observe(viewLifecycleOwner) { comment -> + if(comment == null) { + return@observe + } + binding.comment.also { + it.textViewUsername.text = comment.username + markwon.setMarkdown(it.postContent, comment.textContent) + Picasso.get().load(UrlGen.userProfileImage(comment.userId)).into(it.avatarPicture) + } + binding.newCommentTextField.editText?.setText(comment.textContent) + } + + viewModel.error.observe(viewLifecycleOwner) { + if(it) { + Toast.makeText(requireContext(), "error", Toast.LENGTH_SHORT).show() + viewModel.error.postValue(true) + } + } + + viewModel.finishedEditingComment.observe(viewLifecycleOwner) { + if(it == true) { + viewModel.finishedEditingComment.postValue(null) + requireDialog().dismiss() + } else if(it == false) { + binding.submitCommentButton.isEnabled = false + } + + } + } + + + companion object { + const val TAG = "EditCommentDialogFragment" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_feed.xml b/app/src/main/res/layout-land/fragment_feed.xml index 48e10eb..578bef4 100644 --- a/app/src/main/res/layout-land/fragment_feed.xml +++ b/app/src/main/res/layout-land/fragment_feed.xml @@ -49,7 +49,7 @@ diff --git a/app/src/main/res/layout/fragment_edit_comment.xml b/app/src/main/res/layout/fragment_edit_comment.xml new file mode 100644 index 0000000..d5eafdb --- /dev/null +++ b/app/src/main/res/layout/fragment_edit_comment.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0f2c63..6f30768 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,4 +74,5 @@ Save snapshot Comment posted! Comment failed to post + Edit comment \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 39649f8..bb3237f 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -23,4 +23,11 @@ 30sp + + \ No newline at end of file