From bc0d712fea3963afc1a3a65102014225e679863d Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Wed, 29 May 2024 12:05:03 +0200 Subject: [PATCH 1/7] Update the moderation bottom sheet and make some extension functions of comment --- .../ModerationBottomSheetDialogFragment.kt | 43 +++++++++++- .../NotificationCommentDetailFragment.kt | 5 ++ .../comments/SharedCommentDetailFragment.kt | 68 +++++++++++++++++-- .../ui/comments/SiteCommentDetailFragment.kt | 34 +++------- .../main/res/layout/comment_moderation.xml | 4 ++ 5 files changed, 122 insertions(+), 32 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt index a983a0c877a8..362d7c61bc87 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt @@ -5,14 +5,26 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout +import androidx.core.view.isVisible import com.google.android.material.R import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.wordpress.android.databinding.CommentModerationBinding +import org.wordpress.android.util.extensions.getSerializableCompat +import java.io.Serializable class ModerationBottomSheetDialogFragment : BottomSheetDialogFragment() { private var binding: CommentModerationBinding? = null + private val state by lazy { + arguments?.getSerializableCompat(KEY_STATE) + ?: throw IllegalArgumentException("CommentState not provided") + } + + var onApprovedClicked = {} + var onPendingClicked = {} + var onSpamClicked = {} + var onTrashClicked = {} override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = CommentModerationBinding.inflate(inflater, container, false).apply { @@ -36,6 +48,22 @@ class ModerationBottomSheetDialogFragment : BottomSheetDialogFragment() { behavior.peekHeight = metrics.heightPixels } } + + binding?.setupLayout() + } + + private fun CommentModerationBinding.setupLayout() { + // handle visibilities + buttonApprove.isVisible = state.canModerate + buttonPending.isVisible = state.canModerate + buttonSpam.isVisible = state.canMarkAsSpam + buttonTrash.isVisible = state.canTrash + + // handle clicks + buttonApprove.setOnClickListener { onApprovedClicked() } + buttonPending.setOnClickListener { onPendingClicked } + buttonSpam.setOnClickListener { onSpamClicked() } + buttonTrash.setOnClickListener { onTrashClicked } } override fun onDestroyView() { @@ -45,6 +73,19 @@ class ModerationBottomSheetDialogFragment : BottomSheetDialogFragment() { companion object { const val TAG = "ModerationBottomSheetDialogFragment" - fun newInstance() = ModerationBottomSheetDialogFragment() + private const val KEY_STATE = "state" + fun newInstance(state: CommentState) = ModerationBottomSheetDialogFragment() + .apply { + arguments = Bundle().apply { putSerializable(KEY_STATE, state) } + } } + + /** + * For handling the UI state of the comment moderation bottom sheet + */ + data class CommentState( + val canModerate: Boolean, + val canTrash: Boolean, + val canMarkAsSpam: Boolean, + ) : Serializable } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt index c7351758401a..8e301f2b3cbe 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt @@ -6,12 +6,14 @@ import androidx.core.view.isGone import org.wordpress.android.R import org.wordpress.android.datasets.NotificationsTable import org.wordpress.android.fluxc.tools.FormattableRangeType +import org.wordpress.android.models.Note import org.wordpress.android.ui.comments.unified.CommentIdentifier import org.wordpress.android.ui.comments.unified.CommentSource import org.wordpress.android.ui.engagement.BottomSheetUiState import org.wordpress.android.ui.reader.tracker.ReaderTracker.Companion.SOURCE_NOTIF_COMMENT_USER_PROFILE import org.wordpress.android.util.AppLog import org.wordpress.android.util.ToastUtils +import java.util.EnumSet /** * Used when called from notification list for a comment notification @@ -19,6 +21,9 @@ import org.wordpress.android.util.ToastUtils * It'd be better to have multiple fragments for different sources for different purposes */ class NotificationCommentDetailFragment : SharedCommentDetailFragment() { + override val enabledActions: EnumSet + get() = note.enabledCommentActions + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (savedInstanceState?.getString(KEY_NOTE_ID) != null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt index 133c5d69ef43..be921415ee8f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt @@ -1,17 +1,33 @@ @file:Suppress("DEPRECATION") + package org.wordpress.android.ui.comments import androidx.core.view.isVisible +import com.gravatar.AvatarQueryOptions +import com.gravatar.AvatarUrl +import com.gravatar.types.Email import org.wordpress.android.databinding.CommentPendingBinding import org.wordpress.android.fluxc.model.CommentModel import org.wordpress.android.fluxc.model.CommentStatus +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.models.Note +import org.wordpress.android.models.Note.EnabledActions +import org.wordpress.android.ui.comments.CommentExtension.canLike +import org.wordpress.android.ui.comments.CommentExtension.canMarkAsSpam +import org.wordpress.android.ui.comments.CommentExtension.canModerate +import org.wordpress.android.ui.comments.CommentExtension.canReply +import org.wordpress.android.ui.comments.CommentExtension.canTrash +import org.wordpress.android.ui.comments.CommentExtension.liked +import org.wordpress.android.util.SiteUtils +import org.wordpress.android.util.WPAvatarUtils +import java.util.EnumSet + /** * Used when we want to write Kotlin in [CommentDetailFragment] * Move converted code to this class */ -abstract class SharedCommentDetailFragment:CommentDetailFragment() { +abstract class SharedCommentDetailFragment : CommentDetailFragment() { // it will be non-null after view created when users from a notification // it will be null when users from comment list protected val note: Note @@ -22,9 +38,11 @@ abstract class SharedCommentDetailFragment:CommentDetailFragment() { protected val comment: CommentModel get() = mComment!! + abstract val enabledActions: EnumSet + override fun updateModerationStatus() { val commentStatus = CommentStatus.fromString(comment.status) - when(commentStatus){ + when (commentStatus) { CommentStatus.APPROVED -> {} CommentStatus.UNAPPROVED -> mBinding?.layoutCommentPending?.handlePendingView() CommentStatus.SPAM -> {} @@ -35,8 +53,6 @@ abstract class SharedCommentDetailFragment:CommentDetailFragment() { CommentStatus.UNSPAM -> {} CommentStatus.UNTRASH -> {} } - - mBinding?.layoutCommentPending?.handlePendingView() // todo: remove this line after PR review } private fun CommentPendingBinding.handlePendingView() { @@ -45,7 +61,47 @@ abstract class SharedCommentDetailFragment:CommentDetailFragment() { } private fun showModerationBottomSheet() { - ModerationBottomSheetDialogFragment.newInstance() - .show(childFragmentManager, ModerationBottomSheetDialogFragment.TAG) + ModerationBottomSheetDialogFragment.newInstance( + ModerationBottomSheetDialogFragment.CommentState( + canModerate = enabledActions.canModerate(), + canMarkAsSpam = enabledActions.canMarkAsSpam(), + canTrash = enabledActions.canTrash(), + ) + ).show(childFragmentManager, ModerationBottomSheetDialogFragment.TAG) + } +} + +object CommentExtension { + /** + * Have permission to moderate/reply/spam this comment + */ + fun EnumSet.canModerate(): Boolean = contains(EnabledActions.ACTION_APPROVE) + || contains(EnabledActions.ACTION_UNAPPROVE) + + fun EnumSet.canMarkAsSpam(): Boolean = contains(EnabledActions.ACTION_SPAM) + + fun EnumSet.canReply(): Boolean = contains(EnabledActions.ACTION_REPLY) + + fun EnumSet.canTrash(): Boolean = canModerate() + + fun canEdit(site: SiteModel): Boolean = site.hasCapabilityEditOthersPosts || site.isSelfHostedAdmin + + fun EnumSet.canLike(site: SiteModel): Boolean = + (contains(EnabledActions.ACTION_LIKE_COMMENT) && SiteUtils.isAccessedViaWPComRest(site)) + + fun CommentModel.getAvatarUrl(size: Int): String = when { + authorProfileImageUrl != null -> WPAvatarUtils.rewriteAvatarUrl( + authorProfileImageUrl!!, + size + ) + + authorEmail != null -> AvatarUrl( + Email(authorEmail!!), + AvatarQueryOptions(size, null, null, null) + ).url().toString() + + else -> "" } + + fun CommentModel.liked() = this.iLike } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt index 2c3c3360e617..7a1c4b83f37c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt @@ -3,17 +3,16 @@ package org.wordpress.android.ui.comments import android.os.Bundle import android.view.View import androidx.core.view.isVisible -import com.gravatar.AvatarQueryOptions -import com.gravatar.AvatarUrl -import com.gravatar.types.Email import org.wordpress.android.R import org.wordpress.android.fluxc.model.CommentModel import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.models.Note.EnabledActions +import org.wordpress.android.ui.comments.CommentExtension.getAvatarUrl import org.wordpress.android.ui.comments.unified.CommentIdentifier import org.wordpress.android.ui.comments.unified.CommentSource import org.wordpress.android.ui.engagement.BottomSheetUiState import org.wordpress.android.ui.reader.tracker.ReaderTracker -import org.wordpress.android.util.WPAvatarUtils +import java.util.EnumSet /** * Used when called from comment list @@ -21,6 +20,9 @@ import org.wordpress.android.util.WPAvatarUtils * It'd be better to have multiple fragments for different sources for different purposes */ class SiteCommentDetailFragment : SharedCommentDetailFragment() { + override val enabledActions: EnumSet + get() = EnumSet.allOf(EnabledActions::class.java) + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (savedInstanceState != null) { @@ -30,10 +32,9 @@ class SiteCommentDetailFragment : SharedCommentDetailFragment() { } } - override fun getUserProfileUiState(): BottomSheetUiState.UserProfileUiState { - return BottomSheetUiState.UserProfileUiState( - userAvatarUrl = CommentExtension.getAvatarUrl( - comment, + override fun getUserProfileUiState(): BottomSheetUiState.UserProfileUiState = + BottomSheetUiState.UserProfileUiState( + userAvatarUrl = comment.getAvatarUrl( resources.getDimensionPixelSize(R.dimen.avatar_sz_large) ), blavatarUrl = "", @@ -46,7 +47,6 @@ class SiteCommentDetailFragment : SharedCommentDetailFragment() { siteId = 0L, blogPreviewSource = ReaderTracker.SOURCE_SITE_COMMENTS_USER_PROFILE ) - } override fun getCommentIdentifier(): CommentIdentifier = CommentIdentifier.SiteCommentIdentifier(comment.id, comment.remoteCommentId) @@ -77,19 +77,3 @@ class SiteCommentDetailFragment : SharedCommentDetailFragment() { } } } - -object CommentExtension { - fun getAvatarUrl(comment: CommentModel, size: Int): String = when { - comment.authorProfileImageUrl != null -> WPAvatarUtils.rewriteAvatarUrl( - comment.authorProfileImageUrl!!, - size - ) - - comment.authorEmail != null -> AvatarUrl( - Email(comment.authorEmail!!), - AvatarQueryOptions(size, null, null, null) - ).url().toString() - - else -> "" - } -} diff --git a/WordPress/src/main/res/layout/comment_moderation.xml b/WordPress/src/main/res/layout/comment_moderation.xml index 577bdbc7ce25..4c92d5c31896 100644 --- a/WordPress/src/main/res/layout/comment_moderation.xml +++ b/WordPress/src/main/res/layout/comment_moderation.xml @@ -21,6 +21,7 @@ android:textSize="@dimen/text_sz_large" /> Date: Thu, 30 May 2024 11:55:58 +0200 Subject: [PATCH 2/7] Implement like/unlike a comment --- .../ui/comments/CommentDetailFragment.java | 2 +- .../ui/comments/CommentDetailViewModel.kt | 57 ++++++++++++ .../NotificationCommentDetailFragment.kt | 10 +++ .../comments/SharedCommentDetailFragment.kt | 74 ++++++++++++++-- .../main/res/drawable/baseline_check_16.xml | 5 ++ .../src/main/res/layout/comment_approved.xml | 86 +++++++++++++++++++ .../res/layout/comment_detail_fragment.xml | 6 ++ .../src/main/res/layout/comment_pending.xml | 2 +- WordPress/src/main/res/values/strings.xml | 2 + 9 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt create mode 100644 WordPress/src/main/res/drawable/baseline_check_16.xml create mode 100644 WordPress/src/main/res/layout/comment_approved.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java index e081af2186cd..959c7e9f1f33 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java @@ -136,7 +136,7 @@ public abstract class CommentDetailFragment extends ViewPagerFragment implements @Nullable private OnPostClickListener mOnPostClickListener; @Nullable private OnCommentActionListener mOnCommentActionListener; @Nullable private OnNoteCommentActionListener mOnNoteCommentActionListener; - @Nullable private CommentSource mCommentSource; // this will be non-null when onCreate() + protected CommentSource mCommentSource; // this will be non-null when onCreate() /* * these determine which actions (moderation, replying, marking as spam) to enable diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt new file mode 100644 index 000000000000..006ce2af22ee --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt @@ -0,0 +1,57 @@ +@file:Suppress("DEPRECATION") + +package org.wordpress.android.ui.comments + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineDispatcher +import org.wordpress.android.datasets.wrappers.NotificationsTableWrapper +import org.wordpress.android.fluxc.generated.CommentActionBuilder +import org.wordpress.android.fluxc.model.CommentModel +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.CommentStore.RemoteLikeCommentPayload +import org.wordpress.android.fluxc.store.CommentsStore +import org.wordpress.android.models.Note +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.comments.unified.CommentsStoreAdapter +import org.wordpress.android.ui.notifications.NotificationEvents.OnNoteCommentLikeChanged +import org.wordpress.android.util.EventBusWrapper +import org.wordpress.android.viewmodel.ScopedViewModel +import javax.inject.Inject +import javax.inject.Named + + +@HiltViewModel +class CommentDetailViewModel @Inject constructor( + @Named(BG_THREAD) bgDispatcher: CoroutineDispatcher, + private val commentStore: CommentsStore, + private val commentsStoreAdapter: CommentsStoreAdapter, + private val eventBusWrapper: EventBusWrapper, + private val notificationsTableWrapper: NotificationsTableWrapper, +) : ScopedViewModel(bgDispatcher) { + private val _updatedComment = MutableLiveData() + val updatedComment: LiveData = _updatedComment + + /** + * Like or unlike a comment + * @param comment the comment to like or unlike + * @param site the site the comment belongs to + * @param note the note the comment belongs to, non-null if the comment is from a notification + */ + fun likeComment(comment: CommentModel, site: SiteModel, note: Note? = null) = launch { + val liked = comment.iLike.not() + comment.apply { iLike = liked } + .let { _updatedComment.postValue(it) } + + commentsStoreAdapter.dispatch( + CommentActionBuilder.newLikeCommentAction( + RemoteLikeCommentPayload(site, comment, liked) + ) + ) + + note?.let { + eventBusWrapper.postSticky(OnNoteCommentLikeChanged(note, liked)) + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt index 8e301f2b3cbe..ee7047d1964c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt @@ -4,6 +4,8 @@ import android.os.Bundle import android.view.View import androidx.core.view.isGone import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.analytics.AnalyticsTracker.Stat import org.wordpress.android.datasets.NotificationsTable import org.wordpress.android.fluxc.tools.FormattableRangeType import org.wordpress.android.models.Note @@ -33,6 +35,14 @@ class NotificationCommentDetailFragment : SharedCommentDetailFragment() { } } + override fun sendLikeCommentEvent(liked: Boolean) { + super.sendLikeCommentEvent(liked) + // it should also track the notification liked/unliked event + AnalyticsTracker.track( + if (liked) Stat.NOTIFICATION_LIKED else Stat.NOTIFICATION_UNLIKED + ) + } + override fun getUserProfileUiState(): BottomSheetUiState.UserProfileUiState { val user = mContentMapper.mapToFormattableContentList(note.body.toString()) .find { FormattableRangeType.fromString(it.type) == FormattableRangeType.USER } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt index be921415ee8f..5e783b2f2e68 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt @@ -2,31 +2,41 @@ package org.wordpress.android.ui.comments +import android.os.Bundle +import android.view.View import androidx.core.view.isVisible +import androidx.fragment.app.viewModels import com.gravatar.AvatarQueryOptions import com.gravatar.AvatarUrl import com.gravatar.types.Email +import dagger.hilt.android.AndroidEntryPoint +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker +import org.wordpress.android.databinding.CommentApprovedBinding import org.wordpress.android.databinding.CommentPendingBinding import org.wordpress.android.fluxc.model.CommentModel import org.wordpress.android.fluxc.model.CommentStatus import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.models.Note import org.wordpress.android.models.Note.EnabledActions -import org.wordpress.android.ui.comments.CommentExtension.canLike import org.wordpress.android.ui.comments.CommentExtension.canMarkAsSpam import org.wordpress.android.ui.comments.CommentExtension.canModerate -import org.wordpress.android.ui.comments.CommentExtension.canReply import org.wordpress.android.ui.comments.CommentExtension.canTrash -import org.wordpress.android.ui.comments.CommentExtension.liked +import org.wordpress.android.ui.main.utils.MeGravatarLoader import org.wordpress.android.util.SiteUtils import org.wordpress.android.util.WPAvatarUtils +import org.wordpress.android.util.analytics.AnalyticsUtils +import org.wordpress.android.util.image.ImageType import java.util.EnumSet +import javax.inject.Inject /** * Used when we want to write Kotlin in [CommentDetailFragment] * Move converted code to this class */ +@AndroidEntryPoint abstract class SharedCommentDetailFragment : CommentDetailFragment() { // it will be non-null after view created when users from a notification // it will be null when users from comment list @@ -38,12 +48,35 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { protected val comment: CommentModel get() = mComment!! + protected val site: SiteModel + get() = mSite!! + + protected val viewModel: CommentDetailViewModel by viewModels() + + @Inject + lateinit var accountStore: AccountStore + + @Inject + lateinit var meGravatarLoader: MeGravatarLoader + abstract val enabledActions: EnumSet + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.updatedComment.observe(viewLifecycleOwner) { + mComment = it + updateModerationStatus() + } + } + override fun updateModerationStatus() { + // reset visibilities + mBinding?.layoutCommentPending?.root?.isVisible = false + mBinding?.layoutCommentApproved?.root?.isVisible = false + val commentStatus = CommentStatus.fromString(comment.status) when (commentStatus) { - CommentStatus.APPROVED -> {} + CommentStatus.APPROVED -> mBinding?.layoutCommentApproved?.handleApprovedView() CommentStatus.UNAPPROVED -> mBinding?.layoutCommentPending?.handlePendingView() CommentStatus.SPAM -> {} CommentStatus.TRASH -> {} @@ -56,10 +89,41 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { } private fun CommentPendingBinding.handlePendingView() { - layoutRoot.isVisible = true + root.isVisible = true textMoreOptions.setOnClickListener { showModerationBottomSheet() } } + private fun CommentApprovedBinding.handleApprovedView() { + root.isVisible = true + meGravatarLoader.load( + newAvatarUploaded = false, + avatarUrl = meGravatarLoader.constructGravatarUrl(accountStore.account.avatarUrl), + imageView = imageAvatar, + imageType = ImageType.USER, + injectFilePath = null, + ) + + textLikeComment.text = if (comment.iLike) { + getString(R.string.comment_liked) + } else { + getString(R.string.like_comment) + } + textLikeComment.setOnClickListener { + viewModel.likeComment(comment, site) + sendLikeCommentEvent(comment.iLike.not()) + } + cardReply.setOnClickListener { } + textReply.text = getString(R.string.comment_reply_to_user, comment.authorName) + } + + protected open fun sendLikeCommentEvent(liked: Boolean) { + AnalyticsUtils.trackCommentActionWithSiteDetails( + if (liked) AnalyticsTracker.Stat.COMMENT_LIKED else AnalyticsTracker.Stat.COMMENT_UNLIKED, + mCommentSource.toAnalyticsCommentActionSource(), + site + ) + } + private fun showModerationBottomSheet() { ModerationBottomSheetDialogFragment.newInstance( ModerationBottomSheetDialogFragment.CommentState( diff --git a/WordPress/src/main/res/drawable/baseline_check_16.xml b/WordPress/src/main/res/drawable/baseline_check_16.xml new file mode 100644 index 000000000000..dac4eacb0692 --- /dev/null +++ b/WordPress/src/main/res/drawable/baseline_check_16.xml @@ -0,0 +1,5 @@ + + + diff --git a/WordPress/src/main/res/layout/comment_approved.xml b/WordPress/src/main/res/layout/comment_approved.xml new file mode 100644 index 000000000000..cba0496fcc6d --- /dev/null +++ b/WordPress/src/main/res/layout/comment_approved.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/layout/comment_detail_fragment.xml b/WordPress/src/main/res/layout/comment_detail_fragment.xml index 44860a3c6ff7..c71c2f7e925b 100644 --- a/WordPress/src/main/res/layout/comment_detail_fragment.xml +++ b/WordPress/src/main/res/layout/comment_detail_fragment.xml @@ -23,6 +23,12 @@ layout="@layout/comment_pending" android:layout_width="match_parent" android:layout_height="wrap_content" /> + + Change status Comment pending moderation + Comment Approved Approve comment More options Choose status @@ -503,6 +504,7 @@ Copy link address Comment approved! Comment liked + Like comment Action done! Processing… Liking… From 9d06cc13aa293b38c083aa86e64fa126e52a719f Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Thu, 30 May 2024 14:08:16 +0200 Subject: [PATCH 3/7] Handle the style and the icon for a text view --- .../comments/SharedCommentDetailFragment.kt | 39 +++++++++++++++---- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt index 5e783b2f2e68..5eeadb608905 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt @@ -4,6 +4,8 @@ package org.wordpress.android.ui.comments import android.os.Bundle import android.view.View +import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.fragment.app.viewModels import com.gravatar.AvatarQueryOptions @@ -76,8 +78,8 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { val commentStatus = CommentStatus.fromString(comment.status) when (commentStatus) { - CommentStatus.APPROVED -> mBinding?.layoutCommentApproved?.handleApprovedView() - CommentStatus.UNAPPROVED -> mBinding?.layoutCommentPending?.handlePendingView() + CommentStatus.APPROVED -> mBinding?.layoutCommentApproved?.bindApprovedView() + CommentStatus.UNAPPROVED -> mBinding?.layoutCommentPending?.bindPendingView() CommentStatus.SPAM -> {} CommentStatus.TRASH -> {} CommentStatus.DELETED -> {} @@ -88,12 +90,12 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { } } - private fun CommentPendingBinding.handlePendingView() { + private fun CommentPendingBinding.bindPendingView() { root.isVisible = true textMoreOptions.setOnClickListener { showModerationBottomSheet() } } - private fun CommentApprovedBinding.handleApprovedView() { + private fun CommentApprovedBinding.bindApprovedView() { root.isVisible = true meGravatarLoader.load( newAvatarUploaded = false, @@ -103,11 +105,22 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { injectFilePath = null, ) - textLikeComment.text = if (comment.iLike) { - getString(R.string.comment_liked) + if (comment.iLike) { + handleLikeCommentView( + textLikeComment, + R.color.inline_action_filled, + R.drawable.star_filled, + R.string.comment_liked + ) } else { - getString(R.string.like_comment) + handleLikeCommentView( + textLikeComment, + R.color.menu_more, + R.drawable.star_empty, + R.string.like_comment + ) } + textLikeComment.setOnClickListener { viewModel.likeComment(comment, site) sendLikeCommentEvent(comment.iLike.not()) @@ -116,6 +129,18 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { textReply.text = getString(R.string.comment_reply_to_user, comment.authorName) } + /** + * Handle like comment text based on the liked status + */ + private fun handleLikeCommentView(textView: TextView, colorId: Int, drawableId: Int, stringId: Int) { + textView.text = getString(stringId) + val color = ContextCompat.getColor(textView.context, colorId) + textView.setTextColor(color) + ContextCompat.getDrawable(textView.context, drawableId) + ?.apply { setTint(color) } + ?.let { textView.setCompoundDrawablesWithIntrinsicBounds(it, null, null, null) } + } + protected open fun sendLikeCommentEvent(liked: Boolean) { AnalyticsUtils.trackCommentActionWithSiteDetails( if (liked) AnalyticsTracker.Stat.COMMENT_LIKED else AnalyticsTracker.Stat.COMMENT_UNLIKED, From 9ebfde68ff862b2115e0e4a40a691a10bb74651d Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Thu, 30 May 2024 15:34:17 +0200 Subject: [PATCH 4/7] Implement Approve function --- .../ui/comments/CommentDetailViewModel.kt | 31 ++++++++++++++++++- .../ModerationBottomSheetDialogFragment.kt | 20 +++++++++--- .../NotificationCommentDetailFragment.kt | 2 ++ .../comments/SharedCommentDetailFragment.kt | 13 ++++++-- .../ui/comments/SiteCommentDetailFragment.kt | 1 + .../src/main/res/layout/comment_approved.xml | 4 +-- .../res/layout/comment_detail_fragment.xml | 6 ++-- WordPress/src/main/res/values/styles.xml | 1 + 8 files changed, 67 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt index 006ce2af22ee..88bd0e4ca15e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt @@ -9,7 +9,10 @@ import kotlinx.coroutines.CoroutineDispatcher import org.wordpress.android.datasets.wrappers.NotificationsTableWrapper import org.wordpress.android.fluxc.generated.CommentActionBuilder import org.wordpress.android.fluxc.model.CommentModel +import org.wordpress.android.fluxc.model.CommentStatus import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.comments.CommentsMapper +import org.wordpress.android.fluxc.store.CommentStore import org.wordpress.android.fluxc.store.CommentStore.RemoteLikeCommentPayload import org.wordpress.android.fluxc.store.CommentsStore import org.wordpress.android.models.Note @@ -25,10 +28,11 @@ import javax.inject.Named @HiltViewModel class CommentDetailViewModel @Inject constructor( @Named(BG_THREAD) bgDispatcher: CoroutineDispatcher, - private val commentStore: CommentsStore, + private val commentsStore: CommentsStore, private val commentsStoreAdapter: CommentsStoreAdapter, private val eventBusWrapper: EventBusWrapper, private val notificationsTableWrapper: NotificationsTableWrapper, + private val commentsMapper: CommentsMapper ) : ScopedViewModel(bgDispatcher) { private val _updatedComment = MutableLiveData() val updatedComment: LiveData = _updatedComment @@ -54,4 +58,29 @@ class CommentDetailViewModel @Inject constructor( eventBusWrapper.postSticky(OnNoteCommentLikeChanged(note, liked)) } } + + fun dispatchModerationAction(site: SiteModel, comment: CommentModel, status: CommentStatus) { + commentsStoreAdapter.dispatch( + if (status == CommentStatus.DELETED) { + CommentActionBuilder.newDeleteCommentAction(CommentStore.RemoteCommentPayload(site, comment)) + } else { + CommentActionBuilder.newPushCommentAction(CommentStore.RemoteCommentPayload(site, comment)) + } + ) + + comment.apply { this.status = status.toString() } + .let { _updatedComment.postValue(it) } + } + + /** + * Fetch the latest comment from the server + * @param site the site the comment belongs to + * @param remoteCommentId the remote ID of the comment to fetch + */ + fun fetchComment(site: SiteModel, remoteCommentId: Long) = launch { + val result = commentsStore.fetchComment(site, remoteCommentId, null) + result.data?.comments?.firstOrNull()?.let { + _updatedComment.postValue(commentsMapper.commentEntityToLegacyModel(it)) + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt index 362d7c61bc87..98f9278368e4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt @@ -60,10 +60,22 @@ class ModerationBottomSheetDialogFragment : BottomSheetDialogFragment() { buttonTrash.isVisible = state.canTrash // handle clicks - buttonApprove.setOnClickListener { onApprovedClicked() } - buttonPending.setOnClickListener { onPendingClicked } - buttonSpam.setOnClickListener { onSpamClicked() } - buttonTrash.setOnClickListener { onTrashClicked } + buttonApprove.setOnClickListener { + onApprovedClicked() + dismiss() + } + buttonPending.setOnClickListener { + onPendingClicked() + dismiss() + } + buttonSpam.setOnClickListener { + onSpamClicked() + dismiss() + } + buttonTrash.setOnClickListener { + onTrashClicked() + dismiss() + } } override fun onDestroyView() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt index ee7047d1964c..64f63e9480cd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/NotificationCommentDetailFragment.kt @@ -33,6 +33,8 @@ class NotificationCommentDetailFragment : SharedCommentDetailFragment() { } else { handleNote(requireArguments().getString(KEY_NOTE_ID)!!) } + + viewModel.fetchComment(site, note.commentId) } override fun sendLikeCommentEvent(liked: Boolean) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt index 5eeadb608905..6affe661af0b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt @@ -92,6 +92,13 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { private fun CommentPendingBinding.bindPendingView() { root.isVisible = true + buttonApproveComment.setOnClickListener { + viewModel.dispatchModerationAction( + site, + comment, + CommentStatus.APPROVED + ) + } textMoreOptions.setOnClickListener { showModerationBottomSheet() } } @@ -100,7 +107,7 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { meGravatarLoader.load( newAvatarUploaded = false, avatarUrl = meGravatarLoader.constructGravatarUrl(accountStore.account.avatarUrl), - imageView = imageAvatar, + imageView = replyAvatar, imageType = ImageType.USER, injectFilePath = null, ) @@ -156,7 +163,9 @@ abstract class SharedCommentDetailFragment : CommentDetailFragment() { canMarkAsSpam = enabledActions.canMarkAsSpam(), canTrash = enabledActions.canTrash(), ) - ).show(childFragmentManager, ModerationBottomSheetDialogFragment.TAG) + ).apply { + onApprovedClicked = { viewModel.dispatchModerationAction(site, comment, CommentStatus.APPROVED) } + }.show(childFragmentManager, ModerationBottomSheetDialogFragment.TAG) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt index 7a1c4b83f37c..eae88beca29d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/SiteCommentDetailFragment.kt @@ -30,6 +30,7 @@ class SiteCommentDetailFragment : SharedCommentDetailFragment() { } else { handleComment(requireArguments().getLong(KEY_COMMENT_ID), requireArguments().getInt(KEY_SITE_LOCAL_ID)) } + viewModel.fetchComment(site, comment.remoteCommentId) } override fun getUserProfileUiState(): BottomSheetUiState.UserProfileUiState = diff --git a/WordPress/src/main/res/layout/comment_approved.xml b/WordPress/src/main/res/layout/comment_approved.xml index cba0496fcc6d..a4ef0b0723fa 100644 --- a/WordPress/src/main/res/layout/comment_approved.xml +++ b/WordPress/src/main/res/layout/comment_approved.xml @@ -44,7 +44,7 @@ android:layout_marginVertical="@dimen/margin_medium"> diff --git a/WordPress/src/main/res/layout/comment_detail_fragment.xml b/WordPress/src/main/res/layout/comment_detail_fragment.xml index c71c2f7e925b..282733f2f9ab 100644 --- a/WordPress/src/main/res/layout/comment_detail_fragment.xml +++ b/WordPress/src/main/res/layout/comment_detail_fragment.xml @@ -22,13 +22,15 @@ android:id="@+id/layout_comment_pending" layout="@layout/comment_pending" android:layout_width="match_parent" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content" + android:visibility="gone"/> + android:layout_height="wrap_content" + android:visibility="gone"/> sans-serif false @color/transparent + ?attr/selectableItemBackground @dimen/margin_extra_medium_large @dimen/margin_extra_medium_large 0dp From b71b44073b2eb886d38bbae4c4a5c50bbda991d5 Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Thu, 30 May 2024 15:47:01 +0200 Subject: [PATCH 5/7] Fix lint issues --- .../android/ui/comments/CommentDetailViewModel.kt | 3 --- .../comments/ModerationBottomSheetDialogFragment.kt | 12 +++++++----- .../ui/comments/SharedCommentDetailFragment.kt | 1 - 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt index 88bd0e4ca15e..6aa00926ae66 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher -import org.wordpress.android.datasets.wrappers.NotificationsTableWrapper import org.wordpress.android.fluxc.generated.CommentActionBuilder import org.wordpress.android.fluxc.model.CommentModel import org.wordpress.android.fluxc.model.CommentStatus @@ -24,14 +23,12 @@ import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject import javax.inject.Named - @HiltViewModel class CommentDetailViewModel @Inject constructor( @Named(BG_THREAD) bgDispatcher: CoroutineDispatcher, private val commentsStore: CommentsStore, private val commentsStoreAdapter: CommentsStoreAdapter, private val eventBusWrapper: EventBusWrapper, - private val notificationsTableWrapper: NotificationsTableWrapper, private val commentsMapper: CommentsMapper ) : ScopedViewModel(bgDispatcher) { private val _updatedComment = MutableLiveData() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt index 98f9278368e4..ccaae0bd08bd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/ModerationBottomSheetDialogFragment.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.comments import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -10,14 +11,14 @@ import com.google.android.material.R import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import kotlinx.parcelize.Parcelize import org.wordpress.android.databinding.CommentModerationBinding -import org.wordpress.android.util.extensions.getSerializableCompat -import java.io.Serializable +import org.wordpress.android.util.extensions.getParcelableCompat class ModerationBottomSheetDialogFragment : BottomSheetDialogFragment() { private var binding: CommentModerationBinding? = null private val state by lazy { - arguments?.getSerializableCompat(KEY_STATE) + arguments?.getParcelableCompat(KEY_STATE) ?: throw IllegalArgumentException("CommentState not provided") } @@ -88,16 +89,17 @@ class ModerationBottomSheetDialogFragment : BottomSheetDialogFragment() { private const val KEY_STATE = "state" fun newInstance(state: CommentState) = ModerationBottomSheetDialogFragment() .apply { - arguments = Bundle().apply { putSerializable(KEY_STATE, state) } + arguments = Bundle().apply { putParcelable(KEY_STATE, state) } } } /** * For handling the UI state of the comment moderation bottom sheet */ + @Parcelize data class CommentState( val canModerate: Boolean, val canTrash: Boolean, val canMarkAsSpam: Boolean, - ) : Serializable + ) : Parcelable } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt index 6affe661af0b..448136c009b6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/SharedCommentDetailFragment.kt @@ -33,7 +33,6 @@ import org.wordpress.android.util.image.ImageType import java.util.EnumSet import javax.inject.Inject - /** * Used when we want to write Kotlin in [CommentDetailFragment] * Move converted code to this class From e60a6abc7e0dc684ee08360c36624f5c9e49c4fd Mon Sep 17 00:00:00 2001 From: Jarvis Lin Date: Thu, 30 May 2024 16:42:50 +0200 Subject: [PATCH 6/7] Add tests --- .../ui/comments/CommentDetailViewModel.kt | 9 +- .../src/main/res/layout/comment_approved.xml | 5 +- .../ui/comments/CommentDetailViewModelTest.kt | 117 ++++++++++++++++++ 3 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/comments/CommentDetailViewModelTest.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt index 6aa00926ae66..4fd4ad0d4de0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailViewModel.kt @@ -56,13 +56,12 @@ class CommentDetailViewModel @Inject constructor( } } + /** + * Dispatch a moderation action to the server, it does not include [CommentStatus.DELETED] status + */ fun dispatchModerationAction(site: SiteModel, comment: CommentModel, status: CommentStatus) { commentsStoreAdapter.dispatch( - if (status == CommentStatus.DELETED) { - CommentActionBuilder.newDeleteCommentAction(CommentStore.RemoteCommentPayload(site, comment)) - } else { - CommentActionBuilder.newPushCommentAction(CommentStore.RemoteCommentPayload(site, comment)) - } + CommentActionBuilder.newPushCommentAction(CommentStore.RemoteCommentPayload(site, comment)) ) comment.apply { this.status = status.toString() } diff --git a/WordPress/src/main/res/layout/comment_approved.xml b/WordPress/src/main/res/layout/comment_approved.xml index a4ef0b0723fa..52afb19ce186 100644 --- a/WordPress/src/main/res/layout/comment_approved.xml +++ b/WordPress/src/main/res/layout/comment_approved.xml @@ -31,7 +31,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/margin_extra_large" - android:layout_marginTop="@dimen/margin_large" + android:layout_marginTop="@dimen/margin_extra_small_large" app:cardCornerRadius="20dp" app:cardElevation="0dp" app:strokeColor="@color/divider" @@ -56,7 +56,6 @@ android:id="@+id/text_reply" android:layout_width="0dp" android:layout_height="wrap_content" - android:fontFamily="sans-serif-medium" android:layout_marginStart="@dimen/margin_medium" android:textColor="@color/menu_more" android:lines="1" @@ -80,7 +79,7 @@ android:textSize="@dimen/text_sz_medium" android:drawablePadding="@dimen/margin_small" android:layout_marginBottom="@dimen/margin_extra_medium_large" - tools:text="Like Edna’s comment" + tools:text="Like comment" android:layout_height="wrap_content" app:drawableStartCompat="@drawable/star_empty" /> diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/CommentDetailViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/CommentDetailViewModelTest.kt new file mode 100644 index 000000000000..f463541cc30b --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/CommentDetailViewModelTest.kt @@ -0,0 +1,117 @@ +@file:Suppress("DEPRECATION") + +package org.wordpress.android.ui.comments + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.isA +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.model.CommentModel +import org.wordpress.android.fluxc.model.CommentStatus +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.model.comments.CommentsMapper +import org.wordpress.android.fluxc.persistence.comments.CommentsDao +import org.wordpress.android.fluxc.store.CommentsStore +import org.wordpress.android.models.Note +import org.wordpress.android.ui.comments.unified.CommentsStoreAdapter +import org.wordpress.android.ui.notifications.NotificationEvents +import org.wordpress.android.util.EventBusWrapper + +@ExperimentalCoroutinesApi +class CommentDetailViewModelTest : BaseUnitTest() { + private val commentsStore: CommentsStore = mock() + private val commentsStoreAdapter: CommentsStoreAdapter = mock() + private val eventBusWrapper: EventBusWrapper = mock() + private val commentsMapper: CommentsMapper = mock() + private lateinit var viewModel: CommentDetailViewModel + + @Before + fun setup() { + viewModel = CommentDetailViewModel( + testDispatcher(), + commentsStore, + commentsStoreAdapter, + eventBusWrapper, + commentsMapper + ) + } + + @Test + fun `when like a comment from comment list, then update comment`() { + // Given + val comment: CommentModel = mock() + val site: SiteModel = mock() + + whenever(comment.iLike).thenReturn(false) + + // When + viewModel.likeComment(comment, site) + + // Then + verify(comment).setILike(true) + verify(commentsStoreAdapter).dispatch(any()) + assert(viewModel.updatedComment.value == comment) + } + + @Test + fun `when like a comment from a notification, then update comment and notification`() { + // Given + val comment: CommentModel = mock() + val site: SiteModel = mock() + val note: Note = mock() + + whenever(comment.iLike).thenReturn(false) + + // When + viewModel.likeComment(comment, site, note) + + // Then + verify(comment).setILike(true) + verify(commentsStoreAdapter).dispatch(any()) + verify(eventBusWrapper).postSticky(isA()) + assert(viewModel.updatedComment.value == comment) + } + + @Test + fun `when dispatch moderation action, then update comment`() { + // Given + val comment: CommentModel = mock() + val site: SiteModel = mock() + + // When + viewModel.dispatchModerationAction(site, comment, CommentStatus.APPROVED) + + // Then + verify(commentsStoreAdapter).dispatch(any()) + verify(comment).setStatus(CommentStatus.APPROVED.toString()) + assert(viewModel.updatedComment.value == comment) + } + + @Test + fun `when comment fetched, then update comment`() = test { + // Given + val commentId = 123L + val site: SiteModel = mock() + val comment:CommentModel = mock() + val commentEntity: CommentsDao.CommentEntity = mock() + val result: CommentsStore.CommentsActionPayload = mock() + + whenever(result.data).thenReturn(mock()) + whenever(result.data?.comments).thenReturn(listOf(commentEntity)) + whenever(commentsStore.fetchComment(any(), any(), eq(null))).thenReturn(result) + whenever(commentsMapper.commentEntityToLegacyModel(commentEntity)).thenReturn(comment) + + // When + viewModel.fetchComment(site, commentId) + + // Then + verify(commentsStore).fetchComment(site, commentId, null) + assert(viewModel.updatedComment.value == comment) + } +} From 0b495ec1d6558c8f6b88be3baf61501bb2020579 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Fri, 31 May 2024 16:02:48 +0300 Subject: [PATCH 7/7] Adds NonNull annotation --- .../wordpress/android/ui/comments/CommentDetailFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java index 959c7e9f1f33..ed71c3b2620d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/CommentDetailFragment.java @@ -136,7 +136,7 @@ public abstract class CommentDetailFragment extends ViewPagerFragment implements @Nullable private OnPostClickListener mOnPostClickListener; @Nullable private OnCommentActionListener mOnCommentActionListener; @Nullable private OnNoteCommentActionListener mOnNoteCommentActionListener; - protected CommentSource mCommentSource; // this will be non-null when onCreate() + @NonNull protected CommentSource mCommentSource; // this will be non-null when onCreate() /* * these determine which actions (moderation, replying, marking as spam) to enable