diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt index 24baa0361c09..132e2af293de 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostActionHandler.kt @@ -73,7 +73,8 @@ class PostActionHandler( private val showSnackbar: (SnackbarMessageHolder) -> Unit, private val showToast: (ToastMessageHolder) -> Unit, private val triggerPreviewStateUpdate: (PostListRemotePreviewState, PostInfoType) -> Unit, - private val copyPost: (SiteModel, PostModel, Boolean) -> Unit + private val copyPost: (SiteModel, PostModel, Boolean) -> Unit, + private val syncPublishingFeatureUtils: SyncPublishingFeatureUtils ) { private val criticalPostActionTracker = CriticalPostActionTracker(onStateChanged = { invalidateList.invoke() @@ -207,7 +208,9 @@ class PostActionHandler( return } post.setStatus(DRAFT.toString()) - dispatcher.dispatch(PostActionBuilder.newPushPostAction(RemotePostPayload(post, site))) + dispatcher.dispatch(PostActionBuilder.newPushPostAction( + syncPublishingFeatureUtils.getRemotePostPayloadForPush(RemotePostPayload(post, site)) + )) val localPostId = LocalId(post.id) criticalPostActionTracker.add(localPostId, MOVING_POST_TO_DRAFT) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostConflictResolver.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostConflictResolver.kt index 100a175e00bd..835faac2b491 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostConflictResolver.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostConflictResolver.kt @@ -5,7 +5,10 @@ import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.generated.PostActionBuilder import org.wordpress.android.fluxc.model.PostModel import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.PostStore +import org.wordpress.android.fluxc.store.PostStore.PostErrorType import org.wordpress.android.fluxc.store.PostStore.RemotePostPayload +import org.wordpress.android.fluxc.store.UploadStore import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.utils.UiString.UiStringRes import org.wordpress.android.util.ToastUtils.Duration @@ -23,10 +26,11 @@ class PostConflictResolver( private val invalidateList: () -> Unit, private val checkNetworkConnection: () -> Boolean, private val showSnackbar: (SnackbarMessageHolder) -> Unit, - private val showToast: (ToastMessageHolder) -> Unit + private val showToast: (ToastMessageHolder) -> Unit, + private val uploadStore: UploadStore, + private val postStore: PostStore ) { - private var originalPostCopyForConflictUndo: PostModel? = null - private var localPostIdForFetchingRemoteVersionOfConflictedPost: Int? = null + private var originalPostId: Int? = null fun updateConflictedPostWithRemoteVersion(localPostId: Int) { // We need network connection to load a remote post @@ -36,7 +40,11 @@ class PostConflictResolver( val post = getPostByLocalPostId.invoke(localPostId) if (post != null) { - originalPostCopyForConflictUndo = post.clone() + originalPostId = post.id + post.error = null + post.setIsLocallyChanged(false) + post.setAutoSaveExcerpt(null) + post.setAutoSaveRevisionId(0) dispatcher.dispatch(PostActionBuilder.newFetchPostAction(RemotePostPayload(post, site))) showToast.invoke(ToastMessageHolder(R.string.toast_conflict_updating_post, Duration.SHORT)) } @@ -48,54 +56,38 @@ class PostConflictResolver( return } - // Keep a reference to which post is being updated with the local version so we can avoid showing the conflicted - // label during the undo snackBar. - localPostIdForFetchingRemoteVersionOfConflictedPost = localPostId invalidateList.invoke() val post = getPostByLocalPostId.invoke(localPostId) ?: return + post.error = null + uploadStore.clearUploadErrorForPost(post) - // and now show a snackBar, acting as if the Post was pushed, but effectively push it after the snackbar is gone - var isUndoed = false - val undoAction = { - isUndoed = true - - // Remove the reference for the post being updated and re-show the conflicted label on undo - localPostIdForFetchingRemoteVersionOfConflictedPost = null - invalidateList.invoke() - } - - val onDismissAction = { _: Int -> - if (!isUndoed) { - localPostIdForFetchingRemoteVersionOfConflictedPost = null - PostUtils.trackSavePostAnalytics(post, site) - dispatcher.dispatch(PostActionBuilder.newPushPostAction(RemotePostPayload(post, site))) - } - } val snackBarHolder = SnackbarMessageHolder( - UiStringRes(R.string.snackbar_conflict_web_version_discarded), - UiStringRes(R.string.snackbar_conflict_undo), - undoAction, - onDismissAction + UiStringRes(R.string.snackbar_conflict_web_version_discarded) ) showSnackbar.invoke(snackBarHolder) - } - fun doesPostHaveUnhandledConflict(post: PostModel): Boolean { - // If we are fetching the remote version of a conflicted post, it means it's already being handled - val isFetchingConflictedPost = localPostIdForFetchingRemoteVersionOfConflictedPost != null && - localPostIdForFetchingRemoteVersionOfConflictedPost == post.id - return !isFetchingConflictedPost && PostUtils.isPostInConflictWithRemote(post) + PostUtils.trackSavePostAnalytics(post, site) + val remotePostPayload = RemotePostPayload(post, site) + remotePostPayload.shouldSkipConflictResolutionCheck = true + dispatcher.dispatch(PostActionBuilder.newPushPostAction(remotePostPayload)) } + fun doesPostHaveUnhandledConflict(post: PostModel): Boolean = + uploadStore.getUploadErrorForPost(post)?.postError?.type == PostErrorType.OLD_REVISION || + PostUtils.isPostInConflictWithRemote(post) + fun hasUnhandledAutoSave(post: PostModel): Boolean { return PostUtils.hasAutoSave(post) } fun onPostSuccessfullyUpdated() { - originalPostCopyForConflictUndo?.id?.let { - val updatedPost = getPostByLocalPostId.invoke(it) + originalPostId?.let { id -> + val updatedPost = getPostByLocalPostId.invoke(id) + originalPostId = null // Conflicted post has been successfully updated with its remote version + uploadStore.clearUploadErrorForPost(updatedPost) + postStore.removeLocalRevision(updatedPost) if (!PostUtils.isPostInConflictWithRemote(updatedPost)) { conflictedPostUpdatedWithRemoteVersion() } @@ -103,20 +95,8 @@ class PostConflictResolver( } private fun conflictedPostUpdatedWithRemoteVersion() { - val undoAction = { - // here replace the post with whatever we had before, again - if (originalPostCopyForConflictUndo != null) { - dispatcher.dispatch(PostActionBuilder.newUpdatePostAction(originalPostCopyForConflictUndo)) - } - } - val onDismissAction = { _: Int -> - originalPostCopyForConflictUndo = null - } val snackBarHolder = SnackbarMessageHolder( - UiStringRes(R.string.snackbar_conflict_local_version_discarded), - UiStringRes(R.string.snackbar_conflict_undo), - undoAction, - onDismissAction + UiStringRes(R.string.snackbar_conflict_local_version_discarded) ) showSnackbar.invoke(snackBarHolder) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListEventListener.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListEventListener.kt index f0b1f2029a11..16d7e3067a53 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListEventListener.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListEventListener.kt @@ -185,16 +185,30 @@ class PostListEventListener( fun onPostUploaded(event: OnPostUploaded) { if (event.post != null && event.post.localSiteId == site.id) { if (!isRemotePreviewingFromPostsList.invoke() && !isRemotePreviewingFromEditor(event.post)) { - triggerPostUploadAction.invoke( - PostUploadedSnackbar( - dispatcher, - site, - event.post, - event.isError, - event.isFirstTimePublish, - null + if (event.isError && event.error.type == PostStore.PostErrorType.OLD_REVISION) { + triggerPostUploadAction.invoke( + PostUploadedSnackbar( + dispatcher, + site, + event.post, + event.isError, + event.isFirstTimePublish, + event.error.message, + showRetry = false + ) ) - ) + } else { + triggerPostUploadAction.invoke( + PostUploadedSnackbar( + dispatcher, + site, + event.post, + event.isError, + event.isFirstTimePublish, + null + ) + ) + } } uploadStatusChanged(event.post.id) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt index ec0afbb051f7..c27bf9524922 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostListMainViewModel.kt @@ -82,7 +82,8 @@ class PostListMainViewModel @Inject constructor( private val savePostToDbUseCase: SavePostToDbUseCase, @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher, - private val uploadStarter: UploadStarter + private val uploadStarter: UploadStarter, + private val syncPublishingFeatureUtils: SyncPublishingFeatureUtils ) : ViewModel(), CoroutineScope { private val lifecycleOwner = object : LifecycleOwner { val lifecycleRegistry = LifecycleRegistry(this) @@ -162,7 +163,9 @@ class PostListMainViewModel @Inject constructor( invalidateList = this::invalidateAllLists, checkNetworkConnection = this::checkNetworkConnection, showSnackbar = { _snackBarMessage.postValue(it) }, - showToast = { _toastMessage.postValue(it) } + showToast = { _toastMessage.postValue(it) }, + uploadStore = uploadStore, + postStore = postStore ) } @@ -182,7 +185,8 @@ class PostListMainViewModel @Inject constructor( showSnackbar = { _snackBarMessage.postValue(it) }, showToast = { _toastMessage.postValue(it) }, triggerPreviewStateUpdate = this::updatePreviewAndDialogState, - copyPost = this::copyPost + copyPost = this::copyPost, + syncPublishingFeatureUtils = syncPublishingFeatureUtils ) } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUploadAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUploadAction.kt index 4feb417a253a..28c75e6747c0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUploadAction.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUploadAction.kt @@ -29,7 +29,8 @@ sealed class PostUploadAction { val post: PostModel, val isError: Boolean, val isFirstTimePublish: Boolean, - val errorMessage: String? + val errorMessage: String?, + val showRetry: Boolean = true ) : PostUploadAction() class MediaUploadedSnackbar( @@ -89,7 +90,8 @@ fun handleUploadAction( action.post, action.errorMessage, action.site, - onPublishingCallback + onPublishingCallback, + action.showRetry ) } is PostUploadAction.MediaUploadedSnackbar -> { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/SyncPublishingFeatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/SyncPublishingFeatureUtils.kt new file mode 100644 index 000000000000..cbc5ef6564e3 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/SyncPublishingFeatureUtils.kt @@ -0,0 +1,29 @@ +package org.wordpress.android.ui.posts + +import org.wordpress.android.fluxc.store.PostStore.RemotePostPayload +import org.wordpress.android.util.config.SyncPublishingFeatureConfig +import javax.inject.Inject + +class SyncPublishingFeatureUtils @Inject constructor( + private val syncPublishingFeatureConfig: SyncPublishingFeatureConfig +) { + private fun isSyncPublishingEnabled(): Boolean { + return syncPublishingFeatureConfig.isEnabled() + } + + /** + * This helper function aids in post-conflict resolution. When attempting to edit a post, + * sending the "if_not_modified_since" to the backend will trigger a 409 error if a newer version + * has already been uploaded from another device. This functionality should be encapsulated + * by the SYNC_PUBLISHING feature flag. The function is used to generate the final RemotePostPayload + * that is sent to the backend through PostActionBuilder.newPushPostAction(). By setting the + * shouldSkipConflictResolutionCheck = true, "if_not_modified_since" is not sent to server and the post overwrites + * the remote version. + */ + fun getRemotePostPayloadForPush(payload: RemotePostPayload): RemotePostPayload { + if (isSyncPublishingEnabled().not()) { + payload.shouldSkipConflictResolutionCheck = true + } + return payload + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/PostUploadHandler.java b/WordPress/src/main/java/org/wordpress/android/ui/uploads/PostUploadHandler.java index 16b347394a4e..2448042982a2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/PostUploadHandler.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/PostUploadHandler.java @@ -34,6 +34,7 @@ import org.wordpress.android.fluxc.store.PostStore.RemotePostPayload; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.ui.posts.PostUtils; +import org.wordpress.android.ui.posts.SyncPublishingFeatureUtils; import org.wordpress.android.ui.prefs.AppPrefs; import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.FetchPostStatusFailed; import org.wordpress.android.ui.uploads.AutoSavePostIfNotDraftResult.PostAutoSaveFailed; @@ -83,6 +84,7 @@ public class PostUploadHandler implements UploadHandler, OnAutoSavePo @Inject UploadActionUseCase mUploadActionUseCase; @Inject AutoSavePostIfNotDraftUseCase mAutoSavePostIfNotDraftUseCase; @Inject PostMediaHandler mPostMediaHandler; + @Inject SyncPublishingFeatureUtils mSyncPublishingFeatureUtils; PostUploadHandler(PostUploadNotifier postUploadNotifier) { ((WordPress) WordPress.getContext().getApplicationContext()).component().inject(this); @@ -285,12 +287,18 @@ protected UploadPostTaskResult doInBackground(PostModel... posts) { switch (mUploadActionUseCase.getUploadAction(mPost)) { case UPLOAD: AppLog.d(T.POSTS, "PostUploadHandler - UPLOAD. Post: " + mPost.getTitle()); - mDispatcher.dispatch(PostActionBuilder.newPushPostAction(payload)); + mDispatcher.dispatch( + PostActionBuilder.newPushPostAction( + mSyncPublishingFeatureUtils.getRemotePostPayloadForPush(payload) + ) + ); break; case UPLOAD_AS_DRAFT: mPost.setStatus(PostStatus.DRAFT.toString()); AppLog.d(T.POSTS, "PostUploadHandler - UPLOAD_AS_DRAFT. Post: " + mPost.getTitle()); - mDispatcher.dispatch(PostActionBuilder.newPushPostAction(payload)); + mDispatcher.dispatch(PostActionBuilder.newPushPostAction( + mSyncPublishingFeatureUtils.getRemotePostPayloadForPush(payload) + )); break; case REMOTE_AUTO_SAVE: AppLog.d(T.POSTS, "PostUploadHandler - REMOTE_AUTO_SAVE. Post: " + mPost.getTitle()); @@ -637,7 +645,9 @@ public void handleAutoSavePostIfNotDraftResult(@NonNull AutoSavePostIfNotDraftRe */ post.setStatus(PostStatus.DRAFT.toString()); SiteModel site = mSiteStore.getSiteByLocalId(post.getLocalSiteId()); - mDispatcher.dispatch(PostActionBuilder.newPushPostAction(new RemotePostPayload(post, site))); + mDispatcher.dispatch(PostActionBuilder.newPushPostAction( + mSyncPublishingFeatureUtils.getRemotePostPayloadForPush(new RemotePostPayload(post, site)) + )); } else { throw new IllegalStateException("All AutoSavePostIfNotDraftResult types must be handled"); } @@ -659,15 +669,30 @@ public void onPostUploaded(OnPostUploaded event) { if (event.isError()) { AppLog.w(T.POSTS, "PostUploadHandler > Post upload failed. " + event.error.type + ": " + event.error.message); - Context context = WordPress.getContext(); - String errorMessage = mUiHelpers.getTextOfUiString(context, - UploadUtils.getErrorMessageResIdFromPostError(PostStatus.fromPost(event.post), event.post.isPage(), - event.error, mUploadActionUseCase.isEligibleForAutoUpload(site, event.post))).toString(); - String notificationMessage = UploadUtils.getErrorMessage(context, event.post.isPage(), errorMessage, false); - mPostUploadNotifier.removePostInfoFromForegroundNotification(event.post, - mMediaStore.getMediaForPost(event.post)); - mPostUploadNotifier.incrementUploadedPostCountFromForegroundNotification(event.post); - mPostUploadNotifier.updateNotificationErrorForPost(event.post, site, notificationMessage, 0); + + if (event.error.type != PostStore.PostErrorType.OLD_REVISION) { + Context context = WordPress.getContext(); + String errorMessage = mUiHelpers.getTextOfUiString( + context, + UploadUtils.getErrorMessageResIdFromPostError( + PostStatus.fromPost(event.post), + event.post.isPage(), + event.error, + mUploadActionUseCase.isEligibleForAutoUpload(site, event.post) + ) + ).toString(); + String notificationMessage = UploadUtils.getErrorMessage( + context, + event.post.isPage(), + errorMessage, + false + ); + mPostUploadNotifier.removePostInfoFromForegroundNotification(event.post, + mMediaStore.getMediaForPost(event.post)); + mPostUploadNotifier.incrementUploadedPostCountFromForegroundNotification(event.post); + mPostUploadNotifier.updateNotificationErrorForPost(event.post, site, notificationMessage, 0); + } + sFirstPublishPosts.remove(event.post.getId()); } else { mPostUploadNotifier.incrementUploadedPostCountFromForegroundNotification(event.post); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadActionUseCase.kt b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadActionUseCase.kt index 24b9b37e7520..7eea6aac9909 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadActionUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadActionUseCase.kt @@ -3,6 +3,7 @@ package org.wordpress.android.ui.uploads import dagger.Reusable import org.wordpress.android.fluxc.model.PostImmutableModel import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.PostStore import org.wordpress.android.fluxc.store.UploadStore import org.wordpress.android.ui.posts.PostUtilsWrapper import org.wordpress.android.ui.uploads.UploadActionUseCase.UploadAction.DO_NOTHING @@ -39,7 +40,8 @@ class UploadActionUseCase @Inject constructor( } // Do not auto-upload post which is in conflict with remote - if (postUtilsWrapper.isPostInConflictWithRemote(post)) { + if (uploadStore.getUploadErrorForPost(post)?.postError?.type == PostStore.PostErrorType.OLD_REVISION + || postUtilsWrapper.isPostInConflictWithRemote(post)) { return DO_NOTHING } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadService.java b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadService.java index d4225e5ae061..de8f6bf387bd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadService.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadService.java @@ -30,9 +30,11 @@ import org.wordpress.android.fluxc.store.PostStore; import org.wordpress.android.fluxc.store.PostStore.OnPostChanged; import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded; +import org.wordpress.android.fluxc.store.PostStore.PostErrorType; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.fluxc.store.UploadStore; import org.wordpress.android.fluxc.store.UploadStore.ClearMediaPayload; +import org.wordpress.android.fluxc.store.UploadStore.UploadError; import org.wordpress.android.ui.media.services.MediaUploadReadyListener; import org.wordpress.android.ui.mysite.SelectedSiteRepository; import org.wordpress.android.ui.notifications.SystemNotificationsTracker; @@ -1050,6 +1052,15 @@ private boolean doFinalProcessingOfPosts(Boolean isError, PostModel post) { EventBus.getDefault().post( new PostEvents.PostUploadCanceled(postModel)); } else { + // Do not re-enqueue a post that has already failed with a version conflict + UploadError error = mUploadStore.getUploadErrorForPost(updatedPost); + if (error != null + && error.postError != null + && error.postError.type == PostErrorType.OLD_REVISION + ) { + continue; + } + // Do not re-enqueue a post that has already failed if (isError != null && isError && mUploadStore.isFailedPost(updatedPost)) { continue; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadUtils.java index 42993a53818b..ebd9026db0af 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadUtils.java @@ -97,6 +97,8 @@ UiString getErrorMessageResIdFromPostError(PostStatus postStatus, boolean isPage case UNAUTHORIZED: return isPage ? new UiStringRes(R.string.error_refresh_unauthorized_pages) : new UiStringRes(R.string.error_refresh_unauthorized_posts); + case OLD_REVISION: + return new UiStringRes(R.string.local_post_is_conflicted); case UNSUPPORTED_ACTION: case INVALID_RESPONSE: case GENERIC_ERROR: @@ -468,19 +470,21 @@ public static boolean userCanPublish(SiteModel site) { return !SiteUtils.isAccessedViaWPComRest(site) || site.getHasCapabilityPublishPosts(); } - public static void onPostUploadedSnackbarHandler(final Activity activity, View snackbarAttachView, + public static void onPostUploadedSnackbarHandler(final Activity activity, + View snackbarAttachView, boolean isError, boolean isFirstTimePublish, final PostModel post, final String errorMessage, final SiteModel site, final Dispatcher dispatcher, SnackbarSequencer sequencer, - @Nullable OnPublishingCallback onPublishingCallback) { + @Nullable OnPublishingCallback onPublishingCallback, + final boolean showRetry) { boolean userCanPublish = userCanPublish(site); if (isError) { if (errorMessage != null) { // RETRY only available for Aztec - if (AppPrefs.isAztecEditorEnabled()) { + if (AppPrefs.isAztecEditorEnabled() && showRetry) { UploadUtils.showSnackbarError(snackbarAttachView, errorMessage, R.string.retry, new View.OnClickListener() { @Override diff --git a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadUtilsWrapper.kt b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadUtilsWrapper.kt index 0eda61cebad2..8ee2738e2d8f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadUtilsWrapper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/uploads/UploadUtilsWrapper.kt @@ -59,7 +59,8 @@ class UploadUtilsWrapper @Inject constructor( post: PostModel?, errorMessage: String?, site: SiteModel?, - onPublishingCallback: OnPublishingCallback? = null + onPublishingCallback: OnPublishingCallback? = null, + showRetry: Boolean = true ) = UploadUtils.onPostUploadedSnackbarHandler( activity, snackbarAttachView, @@ -70,7 +71,8 @@ class UploadUtilsWrapper @Inject constructor( site, dispatcher, sequencer, - onPublishingCallback + onPublishingCallback, + showRetry ) @JvmOverloads diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PostModelUploadUiStateUseCase.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PostModelUploadUiStateUseCase.kt index aec9832dae32..e9d3f744d111 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PostModelUploadUiStateUseCase.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PostModelUploadUiStateUseCase.kt @@ -26,18 +26,18 @@ class PostModelUploadUiStateUseCase @Inject constructor() { val postStatus = PostStatus.fromPost(post) val uploadStatus = uploadStatusTracker.getUploadStatus(post, site) return when { - uploadStatus.hasInProgressMediaUpload -> UploadingMedia( - uploadStatus.mediaUploadProgress - ) - uploadStatus.isUploading -> UploadingPost( - postStatus == DRAFT - ) // the upload error is not null on retry -> it needs to be evaluated after UploadingMedia and UploadingPost uploadStatus.uploadError != null -> UploadFailed( uploadStatus.uploadError, uploadStatus.isEligibleForAutoUpload, uploadStatus.uploadWillPushChanges ) + uploadStatus.hasInProgressMediaUpload -> UploadingMedia( + uploadStatus.mediaUploadProgress + ) + uploadStatus.isUploading -> UploadingPost( + postStatus == DRAFT + ) uploadStatus.hasPendingMediaUpload || uploadStatus.isQueued || uploadStatus.isUploadingOrQueued -> UploadQueued diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt index 50647875fcb5..13f41d332241 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/posts/PostListItemUiStateHelper.kt @@ -98,7 +98,6 @@ class PostListItemUiStateHelper @Inject constructor( ): PostListItemUiState { val postStatus: PostStatus = PostStatus.fromPost(post) val uploadUiState = uploadUiStateUseCase.createUploadUiState(post, site, uploadStatusTracker) - val onButtonClicked = { buttonType: PostListButtonType -> onAction.invoke(post, buttonType, POST_LIST_BUTTON_PRESSED) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostConflictResolverTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostConflictResolverTest.kt new file mode 100644 index 000000000000..6039061ceb3f --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostConflictResolverTest.kt @@ -0,0 +1,151 @@ +package org.wordpress.android.ui.posts + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.mock +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.PostModel +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.PostStore +import org.wordpress.android.fluxc.store.UploadStore +import org.wordpress.android.ui.pages.SnackbarMessageHolder +import org.wordpress.android.ui.utils.UiString +import org.wordpress.android.viewmodel.helpers.ToastMessageHolder +import kotlin.test.Test +import org.wordpress.android.R + +@Suppress("UNCHECKED_CAST") +@ExperimentalCoroutinesApi +class PostConflictResolverTest : BaseUnitTest() { + private val dispatcher: Dispatcher = mock() + private val site: SiteModel = mock() + + private val getPostByLocalPostId = mock(Function1::class.java) as (Int) -> PostModel? + private val invalidateList = mock(Function0::class.java) as () -> Unit + private val checkNetworkConnection = mock(Function0::class.java) as () -> Boolean + private val showSnackbar = mock(Function1::class.java) as (SnackbarMessageHolder) -> Unit + private val showToast = mock(Function1::class.java) as (ToastMessageHolder) -> Unit + private val uploadStore: UploadStore = mock() + private val postStore: PostStore = mock() + + // Class under test + private lateinit var postConflictResolver: PostConflictResolver + + @Before + fun setUp() { + postConflictResolver = PostConflictResolver( + dispatcher, + site, + getPostByLocalPostId, + invalidateList, + checkNetworkConnection, + showSnackbar, + showToast, + uploadStore, + postStore + ) + } + + @Test + fun `given network connection, when update conflicted post with local version is invoked, then success`() { + whenever(checkNetworkConnection.invoke()).thenReturn(true) + val post = PostModel() + whenever(getPostByLocalPostId.invoke(anyInt())).thenReturn(post) + val expectedSnackbarMessage = SnackbarMessageHolder( + UiString.UiStringRes(R.string.snackbar_conflict_web_version_discarded) + ) + + postConflictResolver.updateConflictedPostWithLocalVersion(123) + + verify(invalidateList).invoke() + verify(uploadStore).clearUploadErrorForPost(post) + verify(showSnackbar).invoke(expectedSnackbarMessage) + verify(dispatcher).dispatch(any()) + } + + @Test + fun `given no network connection, when update conflicted post with local version is invoked, then no network`() { + whenever(checkNetworkConnection.invoke()).thenReturn(false) + + postConflictResolver.updateConflictedPostWithLocalVersion(123) + + verifyNoInteractions(getPostByLocalPostId) + verifyNoInteractions(postStore) + verifyNoInteractions(showSnackbar) + verifyNoInteractions(dispatcher) + } + + @Test + fun `given upload store with unhandled conflict, when does post have unhandled conflict is invoked, then true`() { + val post = PostModel() + whenever(uploadStore.getUploadErrorForPost(post)).thenReturn( + UploadStore.UploadError(PostStore.PostError(PostStore.PostErrorType.OLD_REVISION)) + ) + + val result = postConflictResolver.doesPostHaveUnhandledConflict(post) + + assertTrue(result) + } + + @Suppress("MaxLineLength") + @Test + fun `given upload store with no unhandled conflict, when post have unhandled conflict is invoked, then false`() { + val post = PostModel() + whenever(uploadStore.getUploadErrorForPost(post)).thenReturn(null) + + val result = postConflictResolver.doesPostHaveUnhandledConflict(post) + + assertFalse(result) + } + + @Test + fun `given post with unhandled auto save, when has unhandled auto save is invoked, then true`() { + val post = PostModel().apply { + setIsLocallyChanged(false) + setAutoSaveRevisionId(1) + setAutoSaveExcerpt("Some auto save excerpt") + } + + val result = postConflictResolver.hasUnhandledAutoSave(post) + + assertTrue(result) + } + + @Test + fun `given post with no unhandled auto save, when has unhandled auto save is invoked, then false`() { + val post = PostModel().apply { + setIsLocallyChanged(true) + } + + val result = postConflictResolver.hasUnhandledAutoSave(post) + + assertFalse(result) + } + + @Test + fun `given post is in conflict with remote, when on post updated, then clear upload error for post`() { + val updatedPost = PostModel() + whenever(getPostByLocalPostId.invoke(anyInt())).thenReturn(updatedPost) + whenever(checkNetworkConnection.invoke()).thenReturn(true) + val expectedSnackbarMessage = SnackbarMessageHolder( + UiString.UiStringRes(R.string.snackbar_conflict_local_version_discarded) + ) + + postConflictResolver.updateConflictedPostWithRemoteVersion(123) + postConflictResolver.onPostSuccessfullyUpdated() + + verify(uploadStore).clearUploadErrorForPost(updatedPost) + verify(showSnackbar).invoke(expectedSnackbarMessage) + verify(postStore).removeLocalRevision(updatedPost) + } +} + diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelCopyPostTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelCopyPostTest.kt index 4dfb685d0a7e..fa36266f0b57 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelCopyPostTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelCopyPostTest.kt @@ -73,7 +73,8 @@ class PostListMainViewModelCopyPostTest : BaseUnitTest() { postListEventListenerFactory = mock(), uploadStarter = mock(), uploadActionUseCase = mock(), - savePostToDbUseCase = mock() + savePostToDbUseCase = mock(), + syncPublishingFeatureUtils = mock() ) viewModel.postListAction.observeForever(onPostListActionObserver) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt index 57bbb125e1b5..e3750f0e56d1 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PostListMainViewModelTest.kt @@ -66,7 +66,8 @@ class PostListMainViewModelTest : BaseUnitTest() { postListEventListenerFactory = mock(), uploadStarter = uploadStarter, uploadActionUseCase = mock(), - savePostToDbUseCase = savePostToDbUseCase + savePostToDbUseCase = savePostToDbUseCase, + syncPublishingFeatureUtils = mock() ) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/SyncPublishingFeatureUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/SyncPublishingFeatureUtilsTest.kt new file mode 100644 index 000000000000..56f249cbaa5e --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/SyncPublishingFeatureUtilsTest.kt @@ -0,0 +1,50 @@ +package org.wordpress.android.ui.posts + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.model.PostModel +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.PostStore.RemotePostPayload +import org.wordpress.android.util.config.SyncPublishingFeatureConfig + +@ExperimentalCoroutinesApi +class SyncPublishingFeatureUtilsTest : BaseUnitTest() { + @Mock + lateinit var syncPublishingFeatureConfig: SyncPublishingFeatureConfig + + private val site: SiteModel = mock() + private val post: PostModel = mock() + + private lateinit var syncPublishingFeatureUtils: SyncPublishingFeatureUtils + + @Before + fun setUp() { + syncPublishingFeatureUtils = SyncPublishingFeatureUtils(syncPublishingFeatureConfig) + } + + @Test + fun `given feature is enabled, when request for payload, then shouldSkipConflictResolution to false`() { + whenever(syncPublishingFeatureConfig.isEnabled()).thenReturn(true) + val remotePostPayload = RemotePostPayload(post, site) + + val result = syncPublishingFeatureUtils.getRemotePostPayloadForPush(remotePostPayload) + + assertThat(result.shouldSkipConflictResolutionCheck).isFalse + } + + @Test + fun `given feature is disabled, when request for payload, then sets shouldSkipConflictResolution to true`() { + whenever(syncPublishingFeatureConfig.isEnabled()).thenReturn(false) + val remotePostPayload = RemotePostPayload(post, site) + + val result = syncPublishingFeatureUtils.getRemotePostPayloadForPush(remotePostPayload) + + assertThat(result.shouldSkipConflictResolutionCheck).isTrue + } +} diff --git a/build.gradle b/build.gradle index 8ff1878da99f..46baee6f28d8 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ ext { automatticTracksVersion = '4.0.2' gutenbergMobileVersion = 'v1.116.0' wordPressAztecVersion = 'v2.1.1' - wordPressFluxCVersion = '2.72.0' + wordPressFluxCVersion = 'trunk-f154a1ace883ee3e0f4b1308a76c9316109b6b03' wordPressLoginVersion = '1.14.1' wordPressPersistentEditTextVersion = '1.0.2' wordPressUtilsVersion = '3.14.0'