diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 4b62d1c14ecf..b01389f22518 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -2,6 +2,7 @@ 19.3 ----- +* [*] Comment editor: Notification comments support in the new editor [https://github.com/wordpress-mobile/WordPress-Android/pull/15884] * [*] Site creation: Fixed bug where sites created within the app were not given the correct time zone, leading to post scheduling issues. [https://github.com/wordpress-mobile/WordPress-Android/pull/15904] 19.2 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 a475d2add681..b87c9b770e71 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 @@ -72,6 +72,10 @@ import org.wordpress.android.ui.ViewPagerFragment; import org.wordpress.android.ui.comments.CommentActions.OnCommentActionListener; import org.wordpress.android.ui.comments.CommentActions.OnNoteCommentActionListener; +import org.wordpress.android.ui.comments.unified.CommentIdentifier; +import org.wordpress.android.ui.comments.unified.CommentIdentifier.NotificationCommentIdentifier; +import org.wordpress.android.ui.comments.unified.CommentIdentifier.SiteCommentIdentifier; +import org.wordpress.android.ui.comments.unified.CommentSource; import org.wordpress.android.ui.comments.unified.CommentsStoreAdapter; import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditActivity; import org.wordpress.android.ui.notifications.NotificationEvents; @@ -101,7 +105,6 @@ import org.wordpress.android.util.ViewUtilsKt; import org.wordpress.android.util.WPLinkMovementMethod; import org.wordpress.android.util.analytics.AnalyticsUtils; -import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource; import org.wordpress.android.util.config.UnifiedCommentsCommentEditFeatureConfig; import org.wordpress.android.util.image.ImageManager; import org.wordpress.android.util.image.ImageType; @@ -137,22 +140,6 @@ public class CommentDetailFragment extends ViewPagerFragment implements Notifica private static final int INTENT_COMMENT_EDITOR = 1010; - enum CommentSource { - NOTIFICATION, - SITE_COMMENTS; - - AnalyticsCommentActionSource toAnalyticsCommentActionSource() { - switch (this) { - case NOTIFICATION: - return AnalyticsCommentActionSource.NOTIFICATIONS; - case SITE_COMMENTS: - return AnalyticsCommentActionSource.SITE_COMMENTS; - } - throw new IllegalArgumentException( - this + " CommentSource is not mapped to corresponding AnalyticsCommentActionSource"); - } - } - private CommentModel mComment; private SiteModel mSite; @@ -668,6 +655,9 @@ private void reloadComment() { if (updatedComment != null) { setComment(updatedComment, mSite); } + if (mNotificationsDetailListFragment != null) { + mNotificationsDetailListFragment.refreshBlocksForEditedComment(mNote.getId()); + } } /** @@ -683,12 +673,10 @@ private void editComment() { // IMPORTANT: don't use getActivity().startActivityForResult() or else onActivityResult() // won't be called in this fragment // https://code.google.com/p/android/issues/detail?id=15394#c45 - if (mUnifiedCommentsCommentEditFeatureConfig.isEnabled() && mCommentSource == CommentSource.SITE_COMMENTS) { - Intent intent = new Intent(getActivity(), UnifiedCommentsEditActivity.class); - intent.putExtra(WordPress.SITE, mSite); - if (mComment != null) { - intent.putExtra(UnifiedCommentsEditActivity.KEY_COMMENT_ID, mComment.getId()); - } + if (mUnifiedCommentsCommentEditFeatureConfig.isEnabled()) { + final CommentIdentifier commentIdentifier = mapCommentIdentifier(); + final Intent intent = + UnifiedCommentsEditActivity.createIntent(requireActivity(), commentIdentifier, mSite); startActivityForResult(intent, INTENT_COMMENT_EDITOR); } else { Intent intent = new Intent(getActivity(), EditCommentActivity.class); @@ -701,6 +689,19 @@ private void editComment() { } } + // TODO [RenanLukas] handle Reader CommentSource when it's ready + @Nullable + private CommentIdentifier mapCommentIdentifier() { + switch (mCommentSource) { + case SITE_COMMENTS: + return new SiteCommentIdentifier(mComment.getId(), mComment.getRemoteCommentId()); + case NOTIFICATION: + return new NotificationCommentIdentifier(mNote.getId(), mNote.getCommentId()); + default: + return null; + } + } + /* * display the current comment */ diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentEssentials.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentEssentials.kt new file mode 100644 index 000000000000..f7d84f785d98 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentEssentials.kt @@ -0,0 +1,19 @@ +package org.wordpress.android.ui.comments.unified + +data class CommentEssentials( + val commentId: Long = DEFAULT_COMMENT_ID, + val userName: String = "", + val commentText: String = "", + val userUrl: String = "", + val userEmail: String = "" +) { + /** + * Checks if this instance of CommentEssentials is valid. An invalid instance should not be used to display data. + * @return true if the instance is valid or false if it's not + */ + fun isValid(): Boolean = commentId > DEFAULT_COMMENT_ID + + companion object { + private const val DEFAULT_COMMENT_ID = -1L + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentIdentifier.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentIdentifier.kt new file mode 100644 index 000000000000..f3da9c995512 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentIdentifier.kt @@ -0,0 +1,27 @@ +package org.wordpress.android.ui.comments.unified + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +sealed class CommentIdentifier : Parcelable { + abstract val remoteCommentId: Long + + @Parcelize + data class SiteCommentIdentifier( + val localCommentId: Int, + override val remoteCommentId: Long + ) : CommentIdentifier() + + @Parcelize + data class ReaderCommentIdentifier( + val blogId: Long, + val postId: Long, + override val remoteCommentId: Long + ) : CommentIdentifier() + + @Parcelize + data class NotificationCommentIdentifier( + val noteId: String, + override val remoteCommentId: Long + ) : CommentIdentifier() +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentSource.kt new file mode 100644 index 000000000000..5c026e926bd4 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/CommentSource.kt @@ -0,0 +1,13 @@ +package org.wordpress.android.ui.comments.unified + +import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource + +enum class CommentSource { + NOTIFICATION, SITE_COMMENTS; + + fun toAnalyticsCommentActionSource(): AnalyticsCommentActionSource = + when (this) { + NOTIFICATION -> AnalyticsCommentActionSource.NOTIFICATIONS + SITE_COMMENTS -> AnalyticsCommentActionSource.SITE_COMMENTS + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditActivity.kt index f36c46448b4f..e6b1f5c546aa 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditActivity.kt @@ -1,5 +1,7 @@ package org.wordpress.android.ui.comments.unified +import android.content.Context +import android.content.Intent import android.os.Bundle import org.wordpress.android.R import org.wordpress.android.WordPress @@ -16,7 +18,7 @@ class UnifiedCommentsEditActivity : LocaleAwareActivity() { } val site = intent.getSerializableExtra(WordPress.SITE) as SiteModel - val commentId: Int = intent.getIntExtra(KEY_COMMENT_ID, 0) + val commentIdentifier = requireNotNull(intent.getParcelableExtra(KEY_COMMENT_IDENTIFIER)) val fm = supportFragmentManager var editCommentFragment = fm.findFragmentByTag( @@ -24,7 +26,7 @@ class UnifiedCommentsEditActivity : LocaleAwareActivity() { ) as? UnifiedCommentsEditFragment if (editCommentFragment == null) { - editCommentFragment = UnifiedCommentsEditFragment.newInstance(site, commentId) + editCommentFragment = UnifiedCommentsEditFragment.newInstance(site, commentIdentifier) fm.beginTransaction() .add(R.id.fragment_container, editCommentFragment, TAG_UNIFIED_EDIT_COMMENT_FRAGMENT) .commit() @@ -32,8 +34,18 @@ class UnifiedCommentsEditActivity : LocaleAwareActivity() { } companion object { - const val KEY_COMMENT_ID = "key_comment_id" - + @JvmStatic + fun createIntent( + context: Context, + commentIdentifier: CommentIdentifier, + siteModel: SiteModel + ): Intent = + Intent(context, UnifiedCommentsEditActivity::class.java).apply { + putExtra(KEY_COMMENT_IDENTIFIER, commentIdentifier) + putExtra(WordPress.SITE, siteModel) + } + + private const val KEY_COMMENT_IDENTIFIER = "key_comment_identifier" private const val TAG_UNIFIED_EDIT_COMMENT_FRAGMENT = "tag_unified_edit_comment_fragment" } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditFragment.kt index 9b1f26a774e2..cfe5ed4514f3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditFragment.kt @@ -55,11 +55,15 @@ class UnifiedCommentsEditFragment : Fragment(R.layout.unified_comments_edit_frag super.onViewCreated(view, savedInstanceState) val site = requireArguments().getSerializable(WordPress.SITE) as SiteModel - val commentId = requireArguments().getInt(KEY_COMMENT_ID) + val commentIdentifier = requireNotNull( + requireArguments().getParcelable( + KEY_COMMENT_IDENTIFIER + ) + ) UnifiedCommentsEditFragmentBinding.bind(view).apply { setupToolbar() - setupObservers(site, commentId) + setupObservers(site, commentIdentifier) } } @@ -89,7 +93,10 @@ class UnifiedCommentsEditFragment : Fragment(R.layout.unified_comments_edit_frag ActivityUtils.hideKeyboardForced(view) } - private fun UnifiedCommentsEditFragmentBinding.setupObservers(site: SiteModel, commentId: Int) { + private fun UnifiedCommentsEditFragmentBinding.setupObservers( + site: SiteModel, + commentIdentifier: CommentIdentifier + ) { viewModel.uiActionEvent.observeEvent(viewLifecycleOwner, { when (it) { CLOSE -> { @@ -142,9 +149,16 @@ class UnifiedCommentsEditFragment : Fragment(R.layout.unified_comments_edit_frag commentEditEmailAddress.error = errors.userEmailError commentEditComment.error = errors.commentTextError } + + with(uiState.inputSettings) { + commentEditComment.isEnabled = enableEditComment + commentEditWebAddress.isEnabled = enableEditUrl + commentEditEmailAddress.isEnabled = enableEditEmail + userName.isEnabled = enableEditName + } }) - viewModel.start(site, commentId) + viewModel.start(site, commentIdentifier) } private fun UnifiedCommentsEditFragmentBinding.showSnackbar(holder: SnackbarMessageHolder) { @@ -210,13 +224,13 @@ class UnifiedCommentsEditFragment : Fragment(R.layout.unified_comments_edit_frag } companion object { - private const val KEY_COMMENT_ID = "key_comment_id" + private const val KEY_COMMENT_IDENTIFIER = "key_comment_identifier" - fun newInstance(site: SiteModel, commentId: Int): UnifiedCommentsEditFragment { + fun newInstance(site: SiteModel, commentIdentifier: CommentIdentifier): UnifiedCommentsEditFragment { val args = Bundle() args.putSerializable(WordPress.SITE, site) - args.putInt(KEY_COMMENT_ID, commentId) + args.putParcelable(KEY_COMMENT_IDENTIFIER, commentIdentifier) val fragment = UnifiedCommentsEditFragment() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt index 78294313a138..0ae5038fef21 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/UnifiedCommentsEditViewModel.kt @@ -8,10 +8,12 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import org.wordpress.android.R import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.persistence.comments.CommentsDao.CommentEntity import org.wordpress.android.fluxc.store.CommentsStore import org.wordpress.android.models.usecases.LocalCommentCacheUpdateHandler import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.modules.UI_THREAD +import org.wordpress.android.ui.comments.unified.CommentIdentifier.NotificationCommentIdentifier import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.EditCommentActionEvent.CANCEL_EDIT_CONFIRM import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.EditCommentActionEvent.CLOSE import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.EditCommentActionEvent.DONE @@ -22,6 +24,9 @@ import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.Fi import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.ProgressState.LOADING import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.ProgressState.NOT_VISIBLE import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.ProgressState.SAVING +import org.wordpress.android.ui.comments.unified.extension.isNotEqualTo +import org.wordpress.android.ui.comments.unified.usecase.GetCommentUseCase +import org.wordpress.android.ui.notifications.utils.NotificationsActionsWrapper import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes @@ -34,13 +39,16 @@ import org.wordpress.android.viewmodel.ScopedViewModel import javax.inject.Inject import javax.inject.Named +@Suppress("TooManyFunctions") class UnifiedCommentsEditViewModel @Inject constructor( @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, private val commentsStore: CommentsStore, private val resourceProvider: ResourceProvider, private val networkUtilsWrapper: NetworkUtilsWrapper, - private val localCommentCacheUpdateHandler: LocalCommentCacheUpdateHandler + private val localCommentCacheUpdateHandler: LocalCommentCacheUpdateHandler, + private val getCommentUseCase: GetCommentUseCase, + private val notificationActionsWrapper: NotificationsActionsWrapper ) : ScopedViewModel(mainDispatcher) { private val _uiState = MutableLiveData() private val _uiActionEvent = MutableLiveData>() @@ -53,6 +61,8 @@ class UnifiedCommentsEditViewModel @Inject constructor( private var isStarted = false private lateinit var site: SiteModel + private lateinit var commentIdentifier: CommentIdentifier + data class EditErrorStrings( val userNameError: String? = null, val commentTextError: String? = null, @@ -60,14 +70,6 @@ class UnifiedCommentsEditViewModel @Inject constructor( val userEmailError: String? = null ) - data class CommentEssentials( - val commentId: Long = 0, - val userName: String = "", - val commentText: String = "", - val userUrl: String = "", - val userEmail: String = "" - ) - data class EditCommentUiState( val canSaveChanges: Boolean, val shouldInitComment: Boolean, @@ -76,7 +78,15 @@ class UnifiedCommentsEditViewModel @Inject constructor( val progressText: UiString? = null, val originalComment: CommentEssentials, val editedComment: CommentEssentials, - val editErrorStrings: EditErrorStrings + val editErrorStrings: EditErrorStrings, + val inputSettings: InputSettings + ) + + data class InputSettings( + val enableEditName: Boolean, + val enableEditUrl: Boolean, + val enableEditEmail: Boolean, + val enableEditComment: Boolean ) enum class ProgressState(val show: Boolean, val progressText: UiString?) { @@ -121,7 +131,7 @@ class UnifiedCommentsEditViewModel @Inject constructor( CANCEL_EDIT_CONFIRM } - fun start(site: SiteModel, commentId: Int) { + fun start(site: SiteModel, commentIdentifier: CommentIdentifier) { if (isStarted) { // If we are here, the fragment view was recreated (like in a configuration change) // so we reattach the watchers. @@ -131,8 +141,9 @@ class UnifiedCommentsEditViewModel @Inject constructor( isStarted = true this.site = site + this.commentIdentifier = commentIdentifier - initViews(commentId) + initViews() } private suspend fun setLoadingState(state: ProgressState) { @@ -144,7 +155,8 @@ class UnifiedCommentsEditViewModel @Inject constructor( progressText = LOADING.progressText, originalComment = CommentEssentials(), editedComment = CommentEssentials(), - editErrorStrings = EditErrorStrings() + editErrorStrings = EditErrorStrings(), + inputSettings = mapInputSettings() ) withContext(mainDispatcher) { @@ -160,34 +172,11 @@ class UnifiedCommentsEditViewModel @Inject constructor( _onSnackbarMessage.value = Event(SnackbarMessageHolder(UiStringRes(R.string.no_network_message))) return } - _uiState.value?.let { uiState -> - val editedContent = uiState.editedComment - + val editedCommentEssentials = uiState.editedComment launch(bgDispatcher) { setLoadingState(SAVING) - - val comment = commentsStore.getCommentByLocalId(editedContent.commentId).firstOrNull() - - comment?.let { - val updatedComment = comment.copy( - authorUrl = editedContent.userUrl, - authorName = editedContent.userName, - authorEmail = editedContent.userEmail, - content = editedContent.commentText - ) - val result = commentsStore.updateEditComment(site, updatedComment) - - if (result.isError) { - setLoadingState(NOT_VISIBLE) - _onSnackbarMessage.postValue( - Event(SnackbarMessageHolder(UiStringRes(R.string.error_edit_comment))) - ) - } else { - _uiActionEvent.postValue(Event(DONE)) - localCommentCacheUpdateHandler.requestCommentsUpdate() - } - } + updateComment(editedCommentEssentials) } } } @@ -206,49 +195,110 @@ class UnifiedCommentsEditViewModel @Inject constructor( _uiActionEvent.value = Event(CLOSE) } - private fun initViews(commentId: Int) { + private fun initViews() { launch { setLoadingState(LOADING) - val commentList = withContext(bgDispatcher) { - commentsStore.getCommentByLocalId(commentId.toLong()) + val commentEssentials = withContext(bgDispatcher) { + mapCommentEssentials() } - - if (commentList.isEmpty()) { + if (commentEssentials.isValid()) { + _uiState.value = + EditCommentUiState( + canSaveChanges = false, + shouldInitComment = true, + shouldInitWatchers = true, + showProgress = LOADING.show, + progressText = LOADING.progressText, + originalComment = commentEssentials, + editedComment = commentEssentials, + editErrorStrings = EditErrorStrings(), + inputSettings = mapInputSettings() + ) + } else { _onSnackbarMessage.value = Event(SnackbarMessageHolder( message = UiStringRes(R.string.error_load_comment), - onDismissAction = { _ -> - _uiActionEvent.value = Event(CLOSE) - } + onDismissAction = { _uiActionEvent.value = Event(CLOSE) } )) - return@launch - } else { - val comment = commentList.first() - val commentEssentials = CommentEssentials( - commentId = comment.id, - userName = comment.authorName ?: "", - commentText = comment.content ?: "", - userUrl = comment.authorUrl ?: "", - userEmail = comment.authorEmail ?: "" - ) - - _uiState.value = EditCommentUiState( - canSaveChanges = false, - shouldInitComment = true, - shouldInitWatchers = true, - showProgress = LOADING.show, - progressText = LOADING.progressText, - originalComment = commentEssentials, - editedComment = commentEssentials, - editErrorStrings = EditErrorStrings() - ) } - delay(LOADING_DELAY_MS) setLoadingState(NOT_VISIBLE) } } + private suspend fun mapCommentEssentials(): CommentEssentials { + val commentEntity = getCommentUseCase.execute(site, commentIdentifier.remoteCommentId) + return if (commentEntity != null) { + CommentEssentials( + commentId = commentEntity.id, + userName = commentEntity.authorName ?: "", + commentText = commentEntity.content ?: "", + userUrl = commentEntity.authorUrl ?: "", + userEmail = commentEntity.authorEmail ?: "" + ) + } else { + CommentEssentials() + } + } + + private suspend fun updateComment(editedCommentEssentials: CommentEssentials) { + val commentEntity = + commentsStore.getCommentByLocalSiteAndRemoteId(site.id, commentIdentifier.remoteCommentId).firstOrNull() + commentEntity?.run { + val isCommentEntityUpdated = updateCommentEntity(this, editedCommentEssentials) + if (isCommentEntityUpdated) { + if (commentIdentifier is NotificationCommentIdentifier) { + updateNotificationEntity() + } else { + _uiActionEvent.postValue(Event(DONE)) + localCommentCacheUpdateHandler.requestCommentsUpdate() + } + } else { + showUpdateCommentError() + } + } ?: showUpdateCommentError() + } + + private suspend fun updateCommentEntity( + comment: CommentEntity, + editedCommentEssentials: CommentEssentials + ): Boolean { + val updatedComment = comment.copy( + authorUrl = editedCommentEssentials.userUrl, + authorName = editedCommentEssentials.userName, + authorEmail = editedCommentEssentials.userEmail, + content = editedCommentEssentials.commentText + ) + val result = commentsStore.updateEditComment(site, updatedComment) + return !result.isError + } + + private suspend fun updateNotificationEntity() { + with(commentIdentifier as NotificationCommentIdentifier) { + val isNotificationEntityUpdated = notificationActionsWrapper.downloadNoteAndUpdateDB(noteId) + if (isNotificationEntityUpdated) { + _uiActionEvent.postValue(Event(DONE)) + localCommentCacheUpdateHandler.requestCommentsUpdate() + } else { + showUpdateNotificationError() + } + } + } + + private suspend fun showUpdateCommentError() { + setLoadingState(NOT_VISIBLE) + _onSnackbarMessage.postValue( + Event(SnackbarMessageHolder(UiStringRes(R.string.error_edit_comment))) + ) + } + + private suspend fun showUpdateNotificationError() { + setLoadingState(NOT_VISIBLE) + _onSnackbarMessage.postValue( + Event(SnackbarMessageHolder(UiStringRes(R.string.error_edit_notification))) + ) + } + fun onValidateField(field: String, fieldType: FieldType) { _uiState.value?.let { val fieldError = if (fieldType.isValid.invoke(field)) { @@ -261,35 +311,35 @@ class UnifiedCommentsEditViewModel @Inject constructor( val previousErrors = it.editErrorStrings val editedComment = previousComment.copy( - userName = if (fieldType.matches(USER_NAME)) field else previousComment.userName, - commentText = if (fieldType.matches(COMMENT)) field else previousComment.commentText, - userUrl = if (fieldType.matches(WEB_ADDRESS)) field else previousComment.userUrl, - userEmail = if (fieldType.matches(USER_EMAIL)) field else previousComment.userEmail + userName = if (fieldType.matches(USER_NAME)) field else previousComment.userName, + commentText = if (fieldType.matches(COMMENT)) field else previousComment.commentText, + userUrl = if (fieldType.matches(WEB_ADDRESS)) field else previousComment.userUrl, + userEmail = if (fieldType.matches(USER_EMAIL)) field else previousComment.userEmail ) val errors = previousErrors.copy( - userNameError = if (fieldType.matches(USER_NAME)) fieldError else previousErrors.userNameError, - commentTextError = if (fieldType.matches(COMMENT)) fieldError else previousErrors.commentTextError, - userUrlError = if (fieldType.matches(WEB_ADDRESS)) fieldError else previousErrors.userUrlError, - userEmailError = if (fieldType.matches(USER_EMAIL)) fieldError else previousErrors.userEmailError + userNameError = if (fieldType.matches(USER_NAME)) fieldError else previousErrors.userNameError, + commentTextError = if (fieldType.matches(COMMENT)) fieldError else previousErrors.commentTextError, + userUrlError = if (fieldType.matches(WEB_ADDRESS)) fieldError else previousErrors.userUrlError, + userEmailError = if (fieldType.matches(USER_EMAIL)) fieldError else previousErrors.userEmailError ) _uiState.value = it.copy( - canSaveChanges = editedComment.isNotEqualTo(it.originalComment) && !errors.hasError(), - shouldInitComment = false, - shouldInitWatchers = false, - editedComment = editedComment, - editErrorStrings = errors + canSaveChanges = editedComment.isNotEqualTo(it.originalComment) && !errors.hasError(), + shouldInitComment = false, + shouldInitWatchers = false, + editedComment = editedComment, + editErrorStrings = errors ) } } - private fun CommentEssentials.isNotEqualTo(other: CommentEssentials): Boolean { - return !(this.commentText == other.commentText && - this.userEmail == other.userEmail && - this.userName == other.userName && - this.userUrl == other.userUrl) - } + private fun mapInputSettings() = InputSettings( + enableEditName = commentIdentifier !is NotificationCommentIdentifier, + enableEditUrl = commentIdentifier !is NotificationCommentIdentifier, + enableEditEmail = commentIdentifier !is NotificationCommentIdentifier, + enableEditComment = true + ) private fun EditErrorStrings.hasError(): Boolean { return listOf( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/extension/CommentEssentialsExtension.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/extension/CommentEssentialsExtension.kt new file mode 100644 index 000000000000..16e066315c38 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/extension/CommentEssentialsExtension.kt @@ -0,0 +1,10 @@ +package org.wordpress.android.ui.comments.unified.extension + +import org.wordpress.android.ui.comments.unified.CommentEssentials + +fun CommentEssentials.isNotEqualTo(other: CommentEssentials): Boolean { + return !(this.commentText == other.commentText && + this.userEmail == other.userEmail && + this.userName == other.userName && + this.userUrl == other.userUrl) +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/usecase/GetCommentUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/usecase/GetCommentUseCase.kt new file mode 100644 index 000000000000..582ed2cf136a --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/comments/unified/usecase/GetCommentUseCase.kt @@ -0,0 +1,22 @@ +package org.wordpress.android.ui.comments.unified.usecase + +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.persistence.comments.CommentsDao.CommentEntity +import org.wordpress.android.fluxc.store.CommentsStore +import javax.inject.Inject + +class GetCommentUseCase @Inject constructor( + private val commentsStore: CommentsStore +) { + suspend fun execute(siteModel: SiteModel, remoteCommentId: Long): CommentEntity? { + val localCommentEntityList = + commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId) + return if (localCommentEntityList.isNullOrEmpty()) { + val commentResponse = commentsStore.fetchComment(siteModel, remoteCommentId, null) + val remoteCommentEntityList = commentResponse.data?.comments + remoteCommentEntityList?.firstOrNull() + } else { + localCommentEntityList.firstOrNull() + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt index cb49789c4faf..65b25eb73588 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/NotificationsDetailListFragment.kt @@ -577,6 +577,11 @@ class NotificationsDetailListFragment : ListFragment(), NotificationFragment { } } + fun refreshBlocksForEditedComment(noteId: String) { + setNote(noteId) + reloadNoteBlocks() + } + // Requests Reader content for certain notification types private fun requestReaderContentForNote() { if (notification == null || !isAdded) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.java index 3509c4f8d2ab..e470b501558c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/blocks/CommentUserNoteBlock.java @@ -10,6 +10,7 @@ import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.core.view.ViewCompat; import org.wordpress.android.R; @@ -29,12 +30,14 @@ public class CommentUserNoteBlock extends UserNoteBlock { private CommentStatus mCommentStatus = CommentStatus.APPROVED; private int mNormalBackgroundColor; private int mIndentedLeftPadding; - + private final Context mContext; private boolean mStatusChanged; - private final FormattableContent mCommentData; + private FormattableContent mCommentData; private final long mTimestamp; + private CommentUserNoteBlockHolder mNoteBlockHolder; + public interface OnCommentStatusChangeListener { void onCommentStatusChanged(CommentStatus newStatus); } @@ -46,7 +49,7 @@ public CommentUserNoteBlock(Context context, FormattableContent noteObject, ImageManager imageManager, NotificationsUtilsWrapper notificationsUtilsWrapper) { super(context, noteObject, onNoteBlockTextClickListener, onGravatarClickedListener, imageManager, notificationsUtilsWrapper); - + mContext = context; mCommentData = commentTextBlock; mTimestamp = timestamp; @@ -68,64 +71,87 @@ public int getLayoutResourceId() { @SuppressLint("ClickableViewAccessibility") // fixed by setting a click listener to avatarImageView @Override public View configureView(View view) { - final CommentUserNoteBlockHolder noteBlockHolder = (CommentUserNoteBlockHolder) view.getTag(); + mNoteBlockHolder = (CommentUserNoteBlockHolder) view.getTag(); + + setUserName(); + setUserCommentAgo(); + setUserCommentSite(); + setUserAvatar(); + setUserComment(); + setCommentStatus(view); + + return view; + } + + private void setUserName() { + mNoteBlockHolder.mNameTextView.setText( + Html.fromHtml("" + getNoteText().toString() + "") + ); + } - noteBlockHolder.mNameTextView - .setText(Html.fromHtml("" + getNoteText().toString() + "")); - noteBlockHolder.mAgoTextView.setText(DateTimeUtils.timeSpanFromTimestamp(getTimestamp(), - noteBlockHolder.mAgoTextView.getContext())); + private void setUserCommentAgo() { + mNoteBlockHolder.mAgoTextView.setText(DateTimeUtils.timeSpanFromTimestamp(getTimestamp(), + mNoteBlockHolder.mAgoTextView.getContext())); + } + + private void setUserCommentSite() { if (!TextUtils.isEmpty(getMetaHomeTitle()) || !TextUtils.isEmpty(getMetaSiteUrl())) { - noteBlockHolder.mBulletTextView.setVisibility(View.VISIBLE); - noteBlockHolder.mSiteTextView.setVisibility(View.VISIBLE); + mNoteBlockHolder.mBulletTextView.setVisibility(View.VISIBLE); + mNoteBlockHolder.mSiteTextView.setVisibility(View.VISIBLE); if (!TextUtils.isEmpty(getMetaHomeTitle())) { - noteBlockHolder.mSiteTextView.setText(getMetaHomeTitle()); + mNoteBlockHolder.mSiteTextView.setText(getMetaHomeTitle()); } else { - noteBlockHolder.mSiteTextView.setText(getMetaSiteUrl().replace("http://", "").replace("https://", "")); + mNoteBlockHolder.mSiteTextView.setText(getMetaSiteUrl().replace("http://", "").replace("https://", "")); } } else { - noteBlockHolder.mBulletTextView.setVisibility(View.GONE); - noteBlockHolder.mSiteTextView.setVisibility(View.GONE); + mNoteBlockHolder.mBulletTextView.setVisibility(View.GONE); + mNoteBlockHolder.mSiteTextView.setVisibility(View.GONE); } + mNoteBlockHolder.mSiteTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + } - noteBlockHolder.mSiteTextView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - + private void setUserAvatar() { String imageUrl = ""; if (hasImageMediaItem()) { imageUrl = GravatarUtils.fixGravatarUrl(getNoteMediaItem().getUrl(), getAvatarSize()); - noteBlockHolder.mAvatarImageView.setContentDescription( - view.getContext() - .getString(R.string.profile_picture, getNoteText().toString())); + mNoteBlockHolder.mAvatarImageView.setContentDescription( + mContext.getString(R.string.profile_picture, getNoteText().toString()) + ); if (!TextUtils.isEmpty(getUserUrl())) { - noteBlockHolder.mAvatarImageView.setOnClickListener(new View.OnClickListener() { + mNoteBlockHolder.mAvatarImageView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { showBlogPreview(); } }); //noinspection AndroidLintClickableViewAccessibility - noteBlockHolder.mAvatarImageView.setOnTouchListener(mOnGravatarTouchListener); + mNoteBlockHolder.mAvatarImageView.setOnTouchListener(mOnGravatarTouchListener); } else { - noteBlockHolder.mAvatarImageView.setOnClickListener(null); + mNoteBlockHolder.mAvatarImageView.setOnClickListener(null); //noinspection AndroidLintClickableViewAccessibility - noteBlockHolder.mAvatarImageView.setOnTouchListener(null); - noteBlockHolder.mAvatarImageView.setContentDescription(null); + mNoteBlockHolder.mAvatarImageView.setOnTouchListener(null); + mNoteBlockHolder.mAvatarImageView.setContentDescription(null); } } else { - noteBlockHolder.mAvatarImageView.setOnClickListener(null); + mNoteBlockHolder.mAvatarImageView.setOnClickListener(null); //noinspection AndroidLintClickableViewAccessibility - noteBlockHolder.mAvatarImageView.setOnTouchListener(null); - noteBlockHolder.mAvatarImageView.setContentDescription(null); + mNoteBlockHolder.mAvatarImageView.setOnTouchListener(null); + mNoteBlockHolder.mAvatarImageView.setContentDescription(null); } - mImageManager.loadIntoCircle(noteBlockHolder.mAvatarImageView, ImageType.AVATAR_WITH_BACKGROUND, imageUrl); + mImageManager.loadIntoCircle(mNoteBlockHolder.mAvatarImageView, ImageType.AVATAR_WITH_BACKGROUND, imageUrl); + } - Spannable spannable = getCommentTextOfNotification(noteBlockHolder); + private void setUserComment() { + Spannable spannable = getCommentTextOfNotification(mNoteBlockHolder); NoteBlockClickableSpan[] spans = spannable.getSpans(0, spannable.length(), NoteBlockClickableSpan.class); for (NoteBlockClickableSpan span : spans) { - span.enableColors(view.getContext()); + span.enableColors(mContext); } - noteBlockHolder.mCommentTextView.setText(spannable); + mNoteBlockHolder.mCommentTextView.setText(spannable); + } + private void setCommentStatus(@NonNull final View view) { // Change display based on comment status and type: // 1. Comment replies are indented and have a 'pipe' background // 2. Unapproved comments have different background and text color @@ -141,27 +167,24 @@ public void onClick(View v) { view.setBackgroundResource(R.drawable.bg_rectangle_warning_surface); } - noteBlockHolder.mDividerView.setVisibility(View.INVISIBLE); + mNoteBlockHolder.mDividerView.setVisibility(View.INVISIBLE); } else { if (hasCommentNestingLevel()) { paddingStart = mIndentedLeftPadding; view.setBackgroundResource(R.drawable.comment_reply_background); - noteBlockHolder.mDividerView.setVisibility(View.INVISIBLE); + mNoteBlockHolder.mDividerView.setVisibility(View.INVISIBLE); } else { view.setBackgroundColor(mNormalBackgroundColor); - noteBlockHolder.mDividerView.setVisibility(View.VISIBLE); + mNoteBlockHolder.mDividerView.setVisibility(View.VISIBLE); } } ViewCompat.setPaddingRelative(view, paddingStart, paddingTop, paddingEnd, paddingBottom); - // If status was changed, fade in the view if (mStatusChanged) { mStatusChanged = false; view.setAlpha(0.4f); view.animate().alpha(1.0f).start(); } - - return view; } private Spannable getCommentTextOfNotification(CommentUserNoteBlockHolder noteBlockHolder) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsActions.java b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsActions.java index e0ef60e59709..92417beb30d0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsActions.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsActions.java @@ -80,7 +80,8 @@ public void onErrorResponse(VolleyError error) { } } - public static void downloadNoteAndUpdateDB(final String noteID, final RestRequest.Listener respoListener, + public static void downloadNoteAndUpdateDB(final String noteID, + final RestRequest.Listener requestListener, final RestRequest.ErrorListener errorListener) { WordPress.getRestClientUtilsV1_1().getNotification( noteID, @@ -104,8 +105,8 @@ public void onResponse(JSONObject response) { AppLog.e(AppLog.T.NOTIFS, "Success, but can't parse the response for the note_id " + noteID, e); } - if (respoListener != null) { - respoListener.onResponse(response); + if (requestListener != null) { + requestListener.onResponse(response); } } }, new RestRequest.ErrorListener() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsActionsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsActionsWrapper.kt new file mode 100644 index 000000000000..25ecd57def95 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/notifications/utils/NotificationsActionsWrapper.kt @@ -0,0 +1,17 @@ +package org.wordpress.android.ui.notifications.utils + +import dagger.Reusable +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +@Reusable +class NotificationsActionsWrapper @Inject constructor() { + suspend fun downloadNoteAndUpdateDB(noteId: String): Boolean = + suspendCoroutine { continuation -> + NotificationsActions.downloadNoteAndUpdateDB( + noteId, + { continuation.resume(true) }, + { continuation.resume(true) }) + } +} diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 33a562eb5ecd..7a258359f557 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1841,6 +1841,7 @@ A network error occurred. Please check your connection and try again. An error occurred while moderating An error occurred while editing the comment + An error occurred while updating the notification content Can\'t publish an empty post Can\'t publish an empty page Can\'t save an empty draft diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentEssentialsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentEssentialsTest.kt new file mode 100644 index 000000000000..6f8c1314a349 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentEssentialsTest.kt @@ -0,0 +1,40 @@ +package org.wordpress.android.ui.comments.unified + +import org.junit.Assert.assertEquals +import org.junit.Test + +class CommentEssentialsTest { + @Test + fun `Should return isValid TRUE if commentId is EQUAL to 0`() { + val expected = true + val actual = CommentEssentials(commentId = 0).isValid() + assertEquals(expected, actual) + } + + @Test + fun `Should return isValid TRUE if commentId is GREATER than 0`() { + val expected = true + val actual = CommentEssentials(commentId = 1).isValid() + assertEquals(expected, actual) + } + + @Test + fun `Should return isValid FALSE if commentId is EQUAL to -1`() { + val expected = false + val actual = CommentEssentials(commentId = -1).isValid() + assertEquals(expected, actual) + } + + @Test + fun `Should return the expected default parameters`() { + val expected = CommentEssentials( + commentId = -1, + userName = "", + commentText = "", + userUrl = "", + userEmail = "" + ) + val actual = CommentEssentials() + assertEquals(expected, actual) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentSourceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentSourceTest.kt new file mode 100644 index 000000000000..110445460296 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/CommentSourceTest.kt @@ -0,0 +1,21 @@ +package org.wordpress.android.ui.comments.unified + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.wordpress.android.util.analytics.AnalyticsUtils.AnalyticsCommentActionSource + +class CommentSourceTest { + @Test + fun `Should return the correct analytics comment action source for NOTIFICATION`() { + val expected = "notifications" + val actual = AnalyticsCommentActionSource.NOTIFICATIONS.toString() + assertEquals(expected, actual) + } + + @Test + fun `Should return the correct analytics comment action source for SITE_COMMENTS`() { + val expected = "site_comments" + val actual = AnalyticsCommentActionSource.SITE_COMMENTS.toString() + assertEquals(expected, actual) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/extension/CommentEssentialsExtensionKtTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/extension/CommentEssentialsExtensionKtTest.kt new file mode 100644 index 000000000000..9a25a1f3daf6 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/extension/CommentEssentialsExtensionKtTest.kt @@ -0,0 +1,67 @@ +package org.wordpress.android.ui.comments.unified.extension + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.wordpress.android.ui.comments.unified.CommentEssentials + +class CommentEssentialsExtensionKtTest { + private val commentEssentials = CommentEssentials( + userName = "name", + commentText = "text", + userUrl = "url", + userEmail = "email" + ) + + @Test + fun `Should return FALSE for isNotEqualTo if commentText, userEmail, userName and userUrl are the SAME`() { + assertThat(commentEssentials.isNotEqualTo(commentEssentials)).isFalse + } + + @Test + fun `Should return TRUE for isNotEqualTo if commentText, userEmail, userName and userUrl are DIFFERENT`() { + val commentEssentials = commentEssentials + val commentEssentials2 = commentEssentials.copy( + userName = "name2", + commentText = "text2", + userUrl = "url2", + userEmail = "email2" + ) + assertThat(commentEssentials.isNotEqualTo(commentEssentials2)).isTrue + } + + @Test + fun `Should return TRUE for isNotEqualTo if ONLY userName is DIFFERENT`() { + val commentEssentials = commentEssentials + val commentEssentials2 = commentEssentials.copy( + userName = "name2" + ) + assertThat(commentEssentials.isNotEqualTo(commentEssentials2)).isTrue + } + + @Test + fun `Should return TRUE for isNotEqualTo if ONLY commentText is DIFFERENT`() { + val commentEssentials = commentEssentials + val commentEssentials2 = commentEssentials.copy( + commentText = "text2" + ) + assertThat(commentEssentials.isNotEqualTo(commentEssentials2)).isTrue + } + + @Test + fun `Should return TRUE for isNotEqualTo if ONLY userUrl is DIFFERENT`() { + val commentEssentials = commentEssentials + val commentEssentials2 = commentEssentials.copy( + userEmail = "url2" + ) + assertThat(commentEssentials.isNotEqualTo(commentEssentials2)).isTrue + } + + @Test + fun `Should return TRUE for isNotEqualTo if ONLY userEmail is DIFFERENT`() { + val commentEssentials = commentEssentials + val commentEssentials2 = commentEssentials.copy( + userEmail = "email2" + ) + assertThat(commentEssentials.isNotEqualTo(commentEssentials2)).isTrue + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/usecase/GetCommentUseCaseTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/usecase/GetCommentUseCaseTest.kt new file mode 100644 index 000000000000..38ca89b58304 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/unified/usecase/GetCommentUseCaseTest.kt @@ -0,0 +1,110 @@ +package org.wordpress.android.ui.comments.unified.usecase + +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.times +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.CommentsStore +import org.wordpress.android.fluxc.store.CommentsStore.CommentsActionPayload +import org.wordpress.android.fluxc.store.CommentsStore.CommentsData.CommentsActionData +import org.wordpress.android.test +import org.wordpress.android.ui.comments.utils.generateMockComments + +class GetCommentUseCaseTest { + private val commentsStore: CommentsStore = mock() + + private val classToTest = GetCommentUseCase(commentsStore) + + private val siteModel = SiteModel() + + private val remoteCommentId = 12345L + + private val commentEntityList = generateMockComments(1) + + @Test + fun `Should get LOCAL comment if found in local DB`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId)) + .thenReturn(generateMockComments(1)) + classToTest.execute(siteModel, remoteCommentId) + verify(commentsStore, times(1)).getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId) + } + + @Test + fun `Should return LOCAL comment if found in local DB`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId)) + .thenReturn(commentEntityList) + val actual = classToTest.execute(siteModel, remoteCommentId) + val expected = commentEntityList.first() + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `Should fetch REMOTE comment if comment NOT found in local DB`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId)) + .thenReturn(emptyList()) + whenever(commentsStore.fetchComment(siteModel, remoteCommentId, null)) + .thenReturn(CommentsActionPayload(CommentsActionData(commentEntityList, 0))) + classToTest.execute(siteModel, remoteCommentId) + verify(commentsStore, times(1)).fetchComment(siteModel, remoteCommentId, null) + } + + @Test + fun `Should return REMOTE comment if comment NOT found in local DB`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId)) + .thenReturn(emptyList()) + whenever(commentsStore.fetchComment(siteModel, remoteCommentId, null)) + .thenReturn(CommentsActionPayload(CommentsActionData(commentEntityList, 0))) + val actual = classToTest.execute(siteModel, remoteCommentId) + val expected = commentEntityList.first() + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `Should return null if LOCAL and REMOTE comment entity lists are EMPTY`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId)) + .thenReturn(emptyList()) + whenever(commentsStore.fetchComment(siteModel, remoteCommentId, null)) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + val actual = classToTest.execute(siteModel, remoteCommentId) + val expected = null + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `Should return null if get LOCAL is null and fetch REMOTE is EMPTY`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId)) + .thenReturn(null) + whenever(commentsStore.fetchComment(siteModel, remoteCommentId, null)) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + val actual = classToTest.execute(siteModel, remoteCommentId) + val expected = null + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `Should return the FIRST element of comment entity LOCAL list`() = test { + val mockComments = generateMockComments(2) + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId)) + .thenReturn(mockComments) + classToTest.execute(siteModel, remoteCommentId) + val actual = classToTest.execute(siteModel, remoteCommentId) + val expected = mockComments.first() + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `Should return the FIRST element of comment entity REMOTE list`() = test { + val mockComments = generateMockComments(2) + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(siteModel.id, remoteCommentId)) + .thenReturn(emptyList()) + whenever(commentsStore.fetchComment(siteModel, remoteCommentId, null)) + .thenReturn(CommentsActionPayload(CommentsActionData(mockComments, 0))) + classToTest.execute(siteModel, remoteCommentId) + val actual = classToTest.execute(siteModel, remoteCommentId) + val expected = mockComments.first() + assertThat(actual).isEqualTo(expected) + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt index f1b369170539..864abf2144e3 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/comments/viewmodels/UnifiedCommentsEditViewModelTest.kt @@ -8,10 +8,12 @@ import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import kotlinx.coroutines.InternalCoroutinesApi import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.mockito.Mock import org.wordpress.android.BaseUnitTest +import org.wordpress.android.R import org.wordpress.android.TEST_DISPATCHER import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.persistence.comments.CommentsDao.CommentEntity @@ -22,8 +24,11 @@ import org.wordpress.android.fluxc.store.CommentsStore.CommentsActionPayload import org.wordpress.android.fluxc.store.CommentsStore.CommentsData.CommentsActionData import org.wordpress.android.models.usecases.LocalCommentCacheUpdateHandler import org.wordpress.android.test +import org.wordpress.android.ui.comments.unified.CommentEssentials +import org.wordpress.android.ui.comments.unified.CommentIdentifier.NotificationCommentIdentifier +import org.wordpress.android.ui.comments.unified.CommentIdentifier.ReaderCommentIdentifier +import org.wordpress.android.ui.comments.unified.CommentIdentifier.SiteCommentIdentifier import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel -import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.CommentEssentials import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.EditCommentActionEvent import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.EditCommentActionEvent.CANCEL_EDIT_CONFIRM import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.EditCommentActionEvent.CLOSE @@ -31,7 +36,10 @@ import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.Ed import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.EditCommentUiState import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.FieldType import org.wordpress.android.ui.comments.unified.UnifiedCommentsEditViewModel.FieldType.USER_EMAIL +import org.wordpress.android.ui.comments.unified.usecase.GetCommentUseCase +import org.wordpress.android.ui.notifications.utils.NotificationsActionsWrapper import org.wordpress.android.ui.pages.SnackbarMessageHolder +import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.NetworkUtilsWrapper import org.wordpress.android.viewmodel.ResourceProvider @@ -41,6 +49,8 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { @Mock lateinit var resourceProvider: ResourceProvider @Mock lateinit var networkUtilsWrapper: NetworkUtilsWrapper @Mock private lateinit var localCommentCacheUpdateHandler: LocalCommentCacheUpdateHandler + @Mock lateinit var getCommentUseCase: GetCommentUseCase + @Mock lateinit var notificationActionsWrapper: NotificationsActionsWrapper private lateinit var viewModel: UnifiedCommentsEditViewModel @@ -48,13 +58,22 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { private var uiActionEvent: MutableList = mutableListOf() private var onSnackbarMessage: MutableList = mutableListOf() - private val site = SiteModel() - private val commentId = 1000 + private val site = SiteModel().apply { + id = LOCAL_SITE_ID + } + + private val localCommentId = 1000 + private val remoteCommentId = 4321L + private val siteCommentIdentifier = SiteCommentIdentifier(localCommentId, remoteCommentId) + private val noteId = "noteId" + private val notificationCommentIdentifier = NotificationCommentIdentifier(noteId, remoteCommentId) @Before fun setup() = test { - whenever(commentsStore.getCommentByLocalId(commentId.toLong())).thenReturn(listOf(DEFAULT_COMMENT)) - whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(true) + whenever(networkUtilsWrapper.isNetworkAvailable()) + .thenReturn(true) + whenever(getCommentUseCase.execute(site, remoteCommentId)) + .thenReturn(COMMENT_ENTITY) viewModel = UnifiedCommentsEditViewModel( mainDispatcher = TEST_DISPATCHER, @@ -62,7 +81,9 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { commentsStore = commentsStore, resourceProvider = resourceProvider, networkUtilsWrapper = networkUtilsWrapper, - localCommentCacheUpdateHandler = localCommentCacheUpdateHandler + localCommentCacheUpdateHandler = localCommentCacheUpdateHandler, + getCommentUseCase = getCommentUseCase, + notificationActionsWrapper = notificationActionsWrapper ) setupObservers() @@ -70,56 +91,93 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { @Test fun `watchers are init on view recreation`() { - viewModel.start(site, commentId) + viewModel.start(site, siteCommentIdentifier) - viewModel.start(site, commentId) + viewModel.start(site, siteCommentIdentifier) assertThat(uiState.first().shouldInitWatchers).isFalse assertThat(uiState.last().shouldInitWatchers).isTrue } @Test - fun `snackbar notification is triggered on comment get error`() = test { - whenever(commentsStore.getCommentByLocalId(commentId.toLong())).thenReturn(listOf()) - viewModel.start(site, commentId) + fun `Should display error SnackBar if mapped CommentEssentials is NOT VALID`() = test { + whenever(getCommentUseCase.execute(site, remoteCommentId)) + .thenReturn(null) + viewModel.start(site, siteCommentIdentifier) assertThat(onSnackbarMessage.firstOrNull()).isNotNull } @Test - fun `view is init`() = test { - viewModel.start(site, commentId) + fun `Should display correct SnackBar error message if mapped CommentEssentials is NOT VALID`() = test { + whenever(getCommentUseCase.execute(site, remoteCommentId)) + .thenReturn(null) + viewModel.start(site, siteCommentIdentifier) + val expected = UiStringRes(R.string.error_load_comment) + val actual = onSnackbarMessage.first().message + assertEquals(expected, actual) + } - verify(commentsStore, times(1)).getCommentByLocalId(commentId.toLong()) + @Test + fun `Should show and hide progress after start`() = test { + viewModel.start(site, siteCommentIdentifier) assertThat(uiState[0].showProgress).isTrue - assertThat(uiState[1].editedComment).isEqualTo(DEFAULT_COMMENT_ESSENTIALS) assertThat(uiState[2].showProgress).isFalse } + @Test + fun `Should get comment from GetCommentUseCase`() = test { + viewModel.start(site, siteCommentIdentifier) + verify(getCommentUseCase).execute(site, remoteCommentId) + } + + @Test + fun `Should map CommentIdentifier to CommentEssentials`() = test { + viewModel.start(site, siteCommentIdentifier) + assertThat(uiState[1].editedComment).isEqualTo(COMMENT_ESSENTIALS) + } + + @Test + fun `Should map CommentIdentifier to default CommentEssentials if CommentIdentifier comment not found`() = test { + whenever(getCommentUseCase.execute(site, remoteCommentId)) + .thenReturn(null) + viewModel.start(site, siteCommentIdentifier) + assertThat(uiState[1].editedComment).isEqualTo(CommentEssentials()) + } + + @Test + fun `Should map CommentIdentifier to default CommentEssentials if CommentIdentifier not handled`() = test { + // ReaderCommentIdentifier is not supported by this class yet + viewModel.start(site, ReaderCommentIdentifier(0L, 0L, 0L)) + assertThat(uiState[1].editedComment).isEqualTo(CommentEssentials()) + } + @Test fun `onActionMenuClicked triggers snackbar if no network`() = test { - whenever(networkUtilsWrapper.isNetworkAvailable()).thenReturn(false) + whenever(networkUtilsWrapper.isNetworkAvailable()) + .thenReturn(false) viewModel.onActionMenuClicked() assertThat(onSnackbarMessage.firstOrNull()).isNotNull } @Test fun `onActionMenuClicked triggers snackbar if comment update error`() = test { - whenever(commentsStore.updateEditComment(eq(site), any())).thenReturn( - CommentsActionPayload(CommentError(GENERIC_ERROR, "error")) - ) - viewModel.start(site, commentId) + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsStore.updateEditComment(eq(site), any())) + .thenReturn(CommentsActionPayload(CommentError(GENERIC_ERROR, "error"))) + viewModel.start(site, siteCommentIdentifier) viewModel.onActionMenuClicked() assertThat(onSnackbarMessage.firstOrNull()).isNotNull } @Test fun `onActionMenuClicked triggers DONE action if comment update successfully`() = test { - whenever(commentsStore.updateEditComment(eq(site), any())).thenReturn(CommentsActionPayload(CommentsActionData( - comments = listOf(), - rowsAffected = 0 - ))) - viewModel.start(site, commentId) + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsStore.updateEditComment(eq(site), any())) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + viewModel.start(site, siteCommentIdentifier) viewModel.onActionMenuClicked() assertThat(uiActionEvent.firstOrNull()).isEqualTo(DONE) verify(localCommentCacheUpdateHandler).requestCommentsUpdate() @@ -127,7 +185,7 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { @Test fun `onBackPressed triggers CLOSE when no edits`() { - viewModel.start(site, commentId) + viewModel.start(site, siteCommentIdentifier) viewModel.onBackPressed() assertThat(uiActionEvent.firstOrNull()).isEqualTo(CLOSE) } @@ -135,10 +193,12 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { @Test fun `onBackPressed triggers CANCEL_EDIT_CONFIRM when edits are present`() { val emailFieldType: FieldType = mock() - whenever(emailFieldType.matches(USER_EMAIL)).thenReturn(true) - whenever(emailFieldType.isValid).thenReturn { _ -> true } + whenever(emailFieldType.matches(USER_EMAIL)) + .thenReturn(true) + whenever(emailFieldType.isValid) + .thenReturn { true } - viewModel.start(site, commentId) + viewModel.start(site, siteCommentIdentifier) viewModel.onValidateField("edited user email", emailFieldType) viewModel.onBackPressed() @@ -151,6 +211,132 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { assertThat(uiActionEvent.firstOrNull()).isEqualTo(CLOSE) } + @Test + fun `Should ENABLE edit name for SiteCommentIdentifier`() { + viewModel.start(site, SiteCommentIdentifier(0, 0L)) + assertThat(uiState.first().inputSettings.enableEditName).isTrue + } + + @Test + fun `Should DISABLE edit name for NotificationCommentIdentifier`() { + viewModel.start(site, NotificationCommentIdentifier("noteId", 0L)) + assertThat(uiState.first().inputSettings.enableEditName).isFalse + } + + @Test + fun `Should ENABLE edit URL for SiteCommentIdentifier`() { + viewModel.start(site, SiteCommentIdentifier(0, 0L)) + assertThat(uiState.first().inputSettings.enableEditUrl).isTrue + } + + @Test + fun `Should DISABLE edit URL for NotificationCommentIdentifier`() { + viewModel.start(site, NotificationCommentIdentifier("noteId", 0L)) + assertThat(uiState.first().inputSettings.enableEditUrl).isFalse + } + + @Test + fun `Should ENABLE edit email for SiteCommentIdentifier`() { + viewModel.start(site, SiteCommentIdentifier(0, 0L)) + assertThat(uiState.first().inputSettings.enableEditEmail).isTrue + } + + @Test + fun `Should DISABLE edit email for NotificationCommentIdentifier`() { + viewModel.start(site, NotificationCommentIdentifier("noteId", 0L)) + assertThat(uiState.first().inputSettings.enableEditEmail).isFalse + } + + @Test + fun `Should ENABLE edit comment content for SiteCommentIdentifier`() { + viewModel.start(site, SiteCommentIdentifier(0, 0L)) + assertThat(uiState.first().inputSettings.enableEditComment).isTrue + } + + @Test + fun `Should ENABLE edit comment content for NotificationCommentIdentifier`() { + viewModel.start(site, NotificationCommentIdentifier("noteId", 0L)) + assertThat(uiState.first().inputSettings.enableEditComment).isTrue + } + + @Test + fun `Should update notification entity on save if NotificationCommentIdentifier`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsStore.updateEditComment(eq(site), any())) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + viewModel.start(site, notificationCommentIdentifier) + viewModel.onActionMenuClicked() + verify(notificationActionsWrapper).downloadNoteAndUpdateDB(noteId) + } + + @Test + fun `Should NOT update notification entity on save if SiteCommentIdentifier`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsStore.updateEditComment(eq(site), any())) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + viewModel.start(site, siteCommentIdentifier) + viewModel.onActionMenuClicked() + verify(notificationActionsWrapper, times(0)).downloadNoteAndUpdateDB(noteId) + } + + @Test + fun `Should trigger DONE action on save if NotificationCommentIdentifier and notification entity update success`() { + test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsStore.updateEditComment(eq(site), any())) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + whenever(notificationActionsWrapper.downloadNoteAndUpdateDB(noteId)) + .thenReturn(true) + viewModel.start(site, notificationCommentIdentifier) + viewModel.onActionMenuClicked() + assertThat(uiActionEvent.firstOrNull()).isEqualTo(DONE) + } + } + + @Test + fun `Should call requestCommentsUpdate() on save if NotificationCommentIdentifier`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsStore.updateEditComment(eq(site), any())) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + whenever(notificationActionsWrapper.downloadNoteAndUpdateDB(noteId)) + .thenReturn(true) + viewModel.start(site, notificationCommentIdentifier) + viewModel.onActionMenuClicked() + verify(localCommentCacheUpdateHandler).requestCommentsUpdate() + } + + @Test + fun `Should display SnackBar error on save if failed to update notification entity`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsStore.updateEditComment(eq(site), any())) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + whenever(notificationActionsWrapper.downloadNoteAndUpdateDB(noteId)) + .thenReturn(false) + viewModel.start(site, notificationCommentIdentifier) + viewModel.onActionMenuClicked() + assertThat(onSnackbarMessage.firstOrNull()).isNotNull + } + + @Test + fun `Should display correct error message on save if failed to update notification entity`() = test { + whenever(commentsStore.getCommentByLocalSiteAndRemoteId(site.id, remoteCommentId)) + .thenReturn(listOf(COMMENT_ENTITY)) + whenever(commentsStore.updateEditComment(eq(site), any())) + .thenReturn(CommentsActionPayload(CommentsActionData(emptyList(), 0))) + whenever(notificationActionsWrapper.downloadNoteAndUpdateDB(noteId)) + .thenReturn(false) + viewModel.start(site, notificationCommentIdentifier) + viewModel.onActionMenuClicked() + val expected = UiStringRes(R.string.error_edit_notification) + val actual = onSnackbarMessage.first().message + assertEquals(expected, actual) + } + private fun setupObservers() { uiState.clear() uiActionEvent.clear() @@ -174,12 +360,14 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { } companion object { - private val DEFAULT_COMMENT = CommentEntity( + private const val LOCAL_SITE_ID = 123 + + private val COMMENT_ENTITY = CommentEntity( id = 1000, remoteCommentId = 0, remotePostId = 0, remoteParentCommentId = 0, - localSiteId = 0, + localSiteId = LOCAL_SITE_ID, remoteSiteId = 0, authorUrl = "authorUrl", authorName = "authorName", @@ -196,12 +384,12 @@ class UnifiedCommentsEditViewModelTest : BaseUnitTest() { iLike = false ) - private val DEFAULT_COMMENT_ESSENTIALS = CommentEssentials( - commentId = DEFAULT_COMMENT.id, - userName = DEFAULT_COMMENT.authorName!!, - commentText = DEFAULT_COMMENT.content!!, - userUrl = DEFAULT_COMMENT.authorUrl!!, - userEmail = DEFAULT_COMMENT.authorEmail!! + private val COMMENT_ESSENTIALS = CommentEssentials( + commentId = COMMENT_ENTITY.id, + userName = COMMENT_ENTITY.authorName!!, + commentText = COMMENT_ENTITY.content!!, + userUrl = COMMENT_ENTITY.authorUrl!!, + userEmail = COMMENT_ENTITY.authorEmail!! ) } }