diff --git a/app/src/main/java/com/isolaatti/home/FeedFragment.kt b/app/src/main/java/com/isolaatti/home/FeedFragment.kt index 2a85687..afd0d65 100644 --- a/app/src/main/java/com/isolaatti/home/FeedFragment.kt +++ b/app/src/main/java/com/isolaatti/home/FeedFragment.kt @@ -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 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 be68cb3..c02716d 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 @@ -9,7 +9,7 @@ import retrofit2.http.Query interface CommentsApi { @POST("Posting/Post/{postId}/Comment") - fun postComment(@Body commentToPost: CommentToPostDto): Call + fun postComment(@Path("postId") postId: Long, @Body commentToPost: CommentToPostDto): Call @GET("Fetch/Post/{postId}/Comments") fun getCommentsOfPosts(@Path("postId") postId: Long, @Query("lastId") lastId: Long, @Query("take") count: Int): Call 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 865d664..acba667 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.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 = flow { - val response = commentsApi.postComment(commentToPostDto).awaitResponse() - emit(response.isSuccessful) + override fun postComment(content: String, audioId: String?, postId: Long): Flow> = 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()) } } \ 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 16d1f62..11f082f 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 @@ -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> fun getComment(commentId: Long): Flow - fun postComment(commentToPostDto: CommentToPostDto, postId: Long): Flow + fun postComment(content: String, audioId: String?, postId: Long): Flow> } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/domain/model/Comment.kt b/app/src/main/java/com/isolaatti/posting/comments/domain/model/Comment.kt index 78de5ea..9982c0c 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/domain/model/Comment.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/domain/model/Comment.kt @@ -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 + ) + } } } diff --git a/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/PostComment.kt b/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/PostComment.kt new file mode 100644 index 0000000..5eadcd0 --- /dev/null +++ b/app/src/main/java/com/isolaatti/posting/comments/domain/use_case/PostComment.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 PostComment @Inject constructor(private val commentsRepository: CommentsRepository) { + operator fun invoke(content: String, postId: Long): Flow> { + return commentsRepository.postComment(content, null, postId) + } +} \ 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 3efc7b8..940beff 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 @@ -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, private val markwon: Markwon, private val callback: OnUserInteractedCallback) : RecyclerView.Adapter() { + 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, 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, 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) { - val lastIndex = if(list.count() - 1 < 1) 0 else list.count() - 1 - list = commentDtoList - notifyItemRangeChanged(lastIndex, commentDtoList.count()) + + + @SuppressLint("NotifyDataSetChanged") + fun updateList(updatedList: List, 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() + } + } } } \ 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 578fe5e..ee3dbea 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 @@ -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> = MutableLiveData() +class CommentsViewModel @Inject constructor(private val getComments: GetComments, private val postComment: PostComment) : ViewModel() { + private val commentsList: MutableList = mutableListOf() - val comments: LiveData> get() = _comments + private val _comments: MutableLiveData, UpdateEvent>> = MutableLiveData() + + val comments: LiveData, UpdateEvent>> get() = _comments + val commentPosted: MutableLiveData = MutableLiveData() + val noMoreContent: MutableLiveData = 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 = mutableListOf(commentDto) - newList.addAll(_comments.value ?: mutableListOf()) - _comments.postValue(newList) + private fun putCommentAtTheBeginning(comment: Comment) { + val newList: MutableList = mutableListOf(comment) + newList.addAll(commentsList) + commentsList.clear() + commentsList.addAll(newList) + _comments.postValue(Pair(commentsList, UpdateEvent(UpdateEvent.UpdateType.COMMENT_ADDED_TOP, null))) } - } \ No newline at end of file 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 new file mode 100644 index 0000000..ce20179 --- /dev/null +++ b/app/src/main/java/com/isolaatti/posting/comments/presentation/UpdateEvent.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/posting/comments/presentation/BottomSheetPostComments.kt b/app/src/main/java/com/isolaatti/posting/comments/ui/BottomSheetPostComments.kt similarity index 67% rename from app/src/main/java/com/isolaatti/posting/comments/presentation/BottomSheetPostComments.kt rename to app/src/main/java/com/isolaatti/posting/comments/ui/BottomSheetPostComments.kt index fcdd7af..cad94e1 100644 --- a/app/src/main/java/com/isolaatti/posting/comments/presentation/BottomSheetPostComments.kt +++ b/app/src/main/java/com/isolaatti/posting/comments/ui/BottomSheetPostComments.kt @@ -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 = 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 = 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt index 44dfeb9..b4d8d6c 100644 --- a/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt +++ b/app/src/main/java/com/isolaatti/profile/ui/ProfileMainFragment.kt @@ -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 diff --git a/app/src/main/java/com/isolaatti/squads/ui/SquadsActivity.kt b/app/src/main/java/com/isolaatti/squads/ui/SquadsActivity.kt new file mode 100644 index 0000000..1705de2 --- /dev/null +++ b/app/src/main/java/com/isolaatti/squads/ui/SquadsActivity.kt @@ -0,0 +1,4 @@ +package com.isolaatti.squads.ui + +class SquadsActivity { +} \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_sheet_post_comments.xml b/app/src/main/res/layout/bottom_sheet_post_comments.xml index 4897fe1..1759840 100644 --- a/app/src/main/res/layout/bottom_sheet_post_comments.xml +++ b/app/src/main/res/layout/bottom_sheet_post_comments.xml @@ -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"