diff --git a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java index 1039c0110a8f..dc41f14ffabb 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/ActivityLauncher.java @@ -556,15 +556,17 @@ public static void editPostOrPageForResult(Intent intent, Activity activity, Sit activity.startActivityForResult(intent, RequestCodes.EDIT_POST); } - public static void editPageForResult(@NonNull Fragment fragment, @NonNull PageModel page) { + public static void editPageForResult(@NonNull Fragment fragment, @NonNull PageModel page, + boolean loadAutoSaveRevision) { Intent intent = new Intent(fragment.getContext(), EditPostActivity.class); - editPageForResult(intent, fragment, page.getSite(), page.getPageId()); + editPageForResult(intent, fragment, page.getSite(), page.getPageId(), loadAutoSaveRevision); } public static void editPageForResult(Intent intent, @NonNull Fragment fragment, @NonNull SiteModel site, - int pageLocalId) { + int pageLocalId, boolean loadAutoSaveRevision) { intent.putExtra(WordPress.SITE, site); intent.putExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, pageLocalId); + intent.putExtra(EditPostActivity.EXTRA_LOAD_AUTO_SAVE_REVISION, loadAutoSaveRevision); fragment.startActivityForResult(intent, RequestCodes.EDIT_POST); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesActivity.kt index d46b421721bd..3888a4a3a163 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesActivity.kt @@ -53,16 +53,20 @@ class PagesActivity : AppCompatActivity(), } override fun onPositiveClicked(instanceTag: String) { - passDeleteConfirmation(instanceTag.toLong()) + val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) + if (fragment is PagesFragment) { + fragment.onPositiveClickedForBasicDialog(instanceTag) + } else { + throw IllegalStateException("PagesFragment is required to consume this event.") + } } override fun onNegativeClicked(instanceTag: String) { - } - - private fun passDeleteConfirmation(remoteId: Long) { val fragment = supportFragmentManager.findFragmentById(R.id.fragment_container) if (fragment is PagesFragment) { - fragment.onPageDeleteConfirmed(remoteId) + fragment.onNegativeClickedForBasicDialog(instanceTag) + } else { + throw IllegalStateException("PagesFragment is required to consume this event.") } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt index dec2c9e56596..b15dba087784 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/pages/PagesFragment.kt @@ -42,8 +42,6 @@ import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask import org.wordpress.android.ui.ActivityLauncher import org.wordpress.android.ui.PagePostCreationSourcesDetail.PAGE_FROM_PAGES_LIST import org.wordpress.android.ui.RequestCodes -import org.wordpress.android.ui.pages.PageItem.Page -import org.wordpress.android.ui.posts.BasicFragmentDialog import org.wordpress.android.ui.posts.EditPostActivity import org.wordpress.android.ui.posts.PostListAction.PreviewPost import org.wordpress.android.ui.posts.PreviewStateHelper @@ -125,7 +123,7 @@ class PagesFragment : Fragment() { if (EditPostActivity.checkToRestart(data)) { ActivityLauncher.editPageForResult(data, this@PagesFragment, viewModel.site, - data.getIntExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, 0)) + data.getIntExtra(EditPostActivity.EXTRA_POST_LOCAL_ID, 0), false) // a restart will happen so, no need to continue here return @@ -314,9 +312,9 @@ class PagesFragment : Fragment() { } }) - viewModel.editPage.observe(this, Observer { page -> + viewModel.editPage.observe(this, Observer { (page, loadAutoRevision) -> page?.let { - ActivityLauncher.editPageForResult(this, page) + ActivityLauncher.editPageForResult(this, page, loadAutoRevision) } }) @@ -345,10 +343,6 @@ class PagesFragment : Fragment() { page?.let { ActivityLauncher.viewPageParentForResult(this, page) } }) - viewModel.displayDeleteDialog.observe(this, Observer { page -> - page?.let { displayDeleteDialog(page) } - }) - viewModel.isNewPageButtonVisible.observe(this, Observer { isVisible -> isVisible?.let { if (isVisible) { @@ -366,6 +360,10 @@ class PagesFragment : Fragment() { (pagesPager.adapter as PagesPagerAdapter).scrollToPage(page) } }) + + viewModel.dialogAction.observe(this, Observer { + it?.show(activity, activity.supportFragmentManager, uiHelpers) + }) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -378,10 +376,6 @@ class PagesFragment : Fragment() { initializeSearchView() } - fun onPageDeleteConfirmed(remoteId: Long) { - viewModel.onDeleteConfirmed(remoteId) - } - private fun refreshProgressBars(listState: PageListState?) { if (!isAdded || view == null) { return @@ -408,16 +402,12 @@ class PagesFragment : Fragment() { } } - private fun displayDeleteDialog(page: Page) { - val dialog = BasicFragmentDialog() - dialog.initialize( - page.id.toString(), - getString(R.string.delete_page), - getString(R.string.page_delete_dialog_message, page.title), - getString(R.string.delete), - getString(R.string.cancel) - ) - dialog.show(fragmentManager, page.id.toString()) + fun onPositiveClickedForBasicDialog(instanceTag: String) { + viewModel.onPositiveClickedForBasicDialog(instanceTag) + } + + fun onNegativeClickedForBasicDialog(instanceTag: String) { + viewModel.onNegativeClickedForBasicDialog(instanceTag) } override fun onStart() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java index bda07b7924c1..413665fc5ff1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostUtils.java @@ -461,7 +461,8 @@ public static String getConflictedPostCustomStringForDialog(PostModel post) { public static UiStringText getCustomStringForAutosaveRevisionDialog(PostModel post) { Context context = WordPress.getContext(); - String firstPart = context.getString(R.string.dialog_confirm_autosave_body_first_part); + String firstPart = post.isPage() ? context.getString(R.string.dialog_confirm_autosave_body_first_part_for_page) + : context.getString(R.string.dialog_confirm_autosave_body_first_part); String lastModified = TextUtils.isEmpty(post.getDateLocallyChanged()) ? post.getLastModified() : post.getDateLocallyChanged(); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/utils/UiHelpers.kt b/WordPress/src/main/java/org/wordpress/android/ui/utils/UiHelpers.kt index c254da41e941..47bfe7625fc3 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/utils/UiHelpers.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/utils/UiHelpers.kt @@ -16,6 +16,7 @@ import org.wordpress.android.ui.utils.UiString.UiStringText import org.wordpress.android.util.DisplayUtils import javax.inject.Inject import android.graphics.Point +import org.wordpress.android.ui.utils.UiString.UiStringResWithParams class UiHelpers @Inject constructor() { fun getPxOfUiDimen(context: Context, uiDimen: UiDimen): Int = @@ -28,6 +29,15 @@ class UiHelpers @Inject constructor() { when (uiString) { is UiStringRes -> context.getString(uiString.stringRes) is UiStringText -> uiString.text + is UiStringResWithParams -> context.getString( + uiString.stringRes, + *uiString.params.map { value -> + getTextOfUiString( + context, + value + ) + }.toTypedArray() + ) } fun updateVisibility(view: View, visible: Boolean) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/utils/UiString.kt b/WordPress/src/main/java/org/wordpress/android/ui/utils/UiString.kt index 12c2320b9edf..b55359658942 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/utils/UiString.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/utils/UiString.kt @@ -9,4 +9,5 @@ import androidx.annotation.StringRes sealed class UiString { data class UiStringText(val text: String) : UiString() data class UiStringRes(@StringRes val stringRes: Int) : UiString() + data class UiStringResWithParams(@StringRes val stringRes: Int, val params: List) : UiString() } diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageConflictResolver.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageConflictResolver.kt new file mode 100644 index 000000000000..439102652107 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageConflictResolver.kt @@ -0,0 +1,11 @@ +package org.wordpress.android.viewmodel.pages + +import org.wordpress.android.fluxc.model.PostModel +import org.wordpress.android.ui.posts.PostUtils +import javax.inject.Inject + +class PageConflictResolver @Inject constructor() { + fun hasUnhandledAutoSave(post: PostModel): Boolean { + return PostUtils.hasAutoSave(post) + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageListDialogHelper.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageListDialogHelper.kt new file mode 100644 index 000000000000..d5a05636e5c9 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PageListDialogHelper.kt @@ -0,0 +1,100 @@ +package org.wordpress.android.viewmodel.pages + +import org.wordpress.android.R +import org.wordpress.android.analytics.AnalyticsTracker.Stat.UNPUBLISHED_REVISION_DIALOG_LOAD_LOCAL_VERSION_CLICKED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.UNPUBLISHED_REVISION_DIALOG_LOAD_UNPUBLISHED_VERSION_CLICKED +import org.wordpress.android.analytics.AnalyticsTracker.Stat.UNPUBLISHED_REVISION_DIALOG_SHOWN +import org.wordpress.android.fluxc.model.LocalOrRemoteId.RemoteId +import org.wordpress.android.fluxc.model.PostModel +import org.wordpress.android.ui.posts.PostUtils +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.ui.utils.UiString.UiStringResWithParams +import org.wordpress.android.ui.utils.UiString.UiStringText +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper +import org.wordpress.android.viewmodel.helpers.DialogHolder +import java.lang.NullPointerException + +private const val CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG = "CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG" +private const val CONFIRM_DELETE_PAGE_DIALOG_TAG = "CONFIRM_DELETE_PAGE_DIALOG_TAG" +private const val POST_TYPE = "post_type" + +class PageListDialogHelper( + private val showDialog: (DialogHolder) -> Unit, + private val analyticsTracker: AnalyticsTrackerWrapper +) { + private var pageIdForAutosaveRevisionResolutionDialog: RemoteId? = null + private var pageIdForDeleteDialog: RemoteId? = null + + fun showAutoSaveRevisionDialog(page: PostModel) { + analyticsTracker.track(UNPUBLISHED_REVISION_DIALOG_SHOWN, mapOf(POST_TYPE to "page")) + val dialogHolder = DialogHolder( + tag = CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG, + title = UiStringRes(R.string.dialog_confirm_autosave_title), + message = PostUtils.getCustomStringForAutosaveRevisionDialog(page), + positiveButton = UiStringRes(R.string.dialog_confirm_autosave_restore_button), + negativeButton = UiStringRes(R.string.dialog_confirm_autosave_dont_restore_button) + ) + pageIdForAutosaveRevisionResolutionDialog = RemoteId(page.remotePostId) + showDialog.invoke(dialogHolder) + } + + fun showDeletePageConfirmationDialog(pageId: RemoteId, pageTitle: String) { + val dialogHolder = DialogHolder( + tag = CONFIRM_DELETE_PAGE_DIALOG_TAG, + title = UiStringRes(R.string.delete_page), + message = UiStringResWithParams( + R.string.page_delete_dialog_message, + listOf(UiStringText(pageTitle)) + ), + positiveButton = UiStringRes(R.string.delete), + negativeButton = UiStringRes(R.string.cancel) + ) + pageIdForDeleteDialog = pageId + showDialog.invoke(dialogHolder) + } + + fun onPositiveClickedForBasicDialog( + instanceTag: String, + deletePage: (RemoteId) -> Unit, + editPage: (RemoteId, LoadAutoSaveRevision) -> Unit + ) { + when (instanceTag) { + CONFIRM_DELETE_PAGE_DIALOG_TAG -> pageIdForDeleteDialog?.let { + pageIdForDeleteDialog = null + deletePage(it) + } ?: throw NullPointerException("pageIdForDeleteDialog shouldn't be null.") + CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG -> pageIdForAutosaveRevisionResolutionDialog?.let { + // open the editor with the restored auto save + pageIdForAutosaveRevisionResolutionDialog = null + editPage(it, true) + analyticsTracker.track( + UNPUBLISHED_REVISION_DIALOG_LOAD_UNPUBLISHED_VERSION_CLICKED, + mapOf(POST_TYPE to "page") + ) + } + ?: throw NullPointerException("pageIdForAutosaveRevisionResolutionDialog shouldn't be null.") + + else -> throw IllegalArgumentException("Dialog's positive button click is not handled: $instanceTag") + } + } + + fun onNegativeClickedForBasicDialog( + instanceTag: String, + editPage: (RemoteId, LoadAutoSaveRevision) -> Unit + ) { + when (instanceTag) { + CONFIRM_DELETE_PAGE_DIALOG_TAG -> pageIdForDeleteDialog = null + CONFIRM_ON_AUTOSAVE_REVISION_DIALOG_TAG -> pageIdForAutosaveRevisionResolutionDialog?.let { + // open the editor with the local page (don't use the auto save version) + editPage(it, false) + analyticsTracker.track( + UNPUBLISHED_REVISION_DIALOG_LOAD_LOCAL_VERSION_CLICKED, + mapOf(POST_TYPE to "page") + ) + } + ?: throw NullPointerException("pageIdForAutosaveRevisionResolutionDialog shouldn't be null.") + + else -> throw IllegalArgumentException("Dialog's negative button click is not handled: $instanceTag") + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt index d0e98d749384..fa5c65e4fb25 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/pages/PagesViewModel.kt @@ -42,10 +42,12 @@ import org.wordpress.android.ui.uploads.UploadStarter import org.wordpress.android.util.AppLog import org.wordpress.android.util.EventBusWrapper import org.wordpress.android.util.NetworkUtilsWrapper +import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper import org.wordpress.android.util.analytics.AnalyticsUtils import org.wordpress.android.util.coroutines.suspendCoroutineWithTimeout import org.wordpress.android.viewmodel.ScopedViewModel import org.wordpress.android.viewmodel.SingleLiveEvent +import org.wordpress.android.viewmodel.helpers.DialogHolder import org.wordpress.android.viewmodel.pages.ActionPerformer.PageAction import org.wordpress.android.viewmodel.pages.ActionPerformer.PageAction.EventType.DELETE import org.wordpress.android.viewmodel.pages.ActionPerformer.PageAction.EventType.UPDATE @@ -73,6 +75,8 @@ private const val SNACKBAR_DELAY = 500L private const val SEARCH_COLLAPSE_DELAY = 500L private const val PAGE_UPLOAD_TIMEOUT = 5000L +typealias LoadAutoSaveRevision = Boolean + class PagesViewModel @Inject constructor( private val pageStore: PageStore, @@ -83,6 +87,8 @@ class PagesViewModel private val eventBusWrapper: EventBusWrapper, private val previewStateHelper: PreviewStateHelper, private val uploadStarter: UploadStarter, + private val analyticsTracker: AnalyticsTrackerWrapper, + private val pageConflictResolver: PageConflictResolver, private val pageListEventListenerFactory: PageListEventListener.Factory, @Named(UI_THREAD) private val uiDispatcher: CoroutineDispatcher, @Named(BG_THREAD) private val defaultDispatcher: CoroutineDispatcher @@ -93,9 +99,6 @@ class PagesViewModel private val _listState = MutableLiveData() val listState: LiveData = _listState - private val _displayDeleteDialog = SingleLiveEvent() - val displayDeleteDialog: LiveData = _displayDeleteDialog - private val _isNewPageButtonVisible = MutableLiveData() val isNewPageButtonVisible: LiveData = _isNewPageButtonVisible @@ -108,8 +111,8 @@ class PagesViewModel private val _createNewPage = SingleLiveEvent() val createNewPage: LiveData = _createNewPage - private val _editPage = SingleLiveEvent() - val editPage: LiveData = _editPage + private val _editPage = SingleLiveEvent>() + val editPage: LiveData> = _editPage private val _previewPage = SingleLiveEvent() val previewPage: LiveData = _previewPage @@ -145,6 +148,9 @@ class PagesViewModel private val _showSnackbarMessage = SingleLiveEvent() val showSnackbarMessage: LiveData = _showSnackbarMessage + private val _dialogAction = SingleLiveEvent() + val dialogAction: LiveData = _dialogAction + private var _site: SiteModel? = null val site: SiteModel get() = checkNotNull(_site) { "Trying to access unitialized site" } @@ -163,6 +169,13 @@ class PagesViewModel private lateinit var pageListEventListener: PageListEventListener + private val pageListDialogHelper: PageListDialogHelper by lazy { + PageListDialogHelper( + showDialog = { _dialogAction.postValue(it) }, + analyticsTracker = analyticsTracker + ) + } + data class BrowsePreview( val post: PostModel, val previewType: RemotePreviewType @@ -384,7 +397,7 @@ class PagesViewModel private fun deletePage(page: Page) { performIfNetworkAvailable { - _displayDeleteDialog.postValue(page) + pageListDialogHelper.showDeletePageConfirmationDialog(RemoteId(page.id), page.title) } } @@ -450,14 +463,17 @@ class PagesViewModel } } - fun onDeleteConfirmed(remoteId: Long) { - launch(defaultDispatcher) { - pageMap[remoteId]?.let { deletePage(it) } - } - } - fun onItemTapped(pageItem: Page) { - _editPage.postValue(pageMap[pageItem.id]) + // TODO We are going to be doing a refactor of the ViewModels related to Pages so that the PostModel is + // available without doing subsequent fetches from the PostStore + // https://github.com/wordpress-mobile/WordPress-Android/issues/11233 + val page = postStore.getPostByRemotePostId(pageItem.id, site) + // Then check if an autosave revision is available + if (pageConflictResolver.hasUnhandledAutoSave(page)) { + pageListDialogHelper.showAutoSaveRevisionDialog(page) + return + } + editPage(RemoteId(pageItem.id)) } fun onNewPageButtonTapped() { @@ -684,6 +700,33 @@ class PagesViewModel } } + // BasicFragmentDialog Events + + fun onPositiveClickedForBasicDialog(instanceTag: String) { + pageListDialogHelper.onPositiveClickedForBasicDialog( + instanceTag = instanceTag, + editPage = this::editPage, + deletePage = this::onDeleteConfirmed + ) + } + + fun onNegativeClickedForBasicDialog(instanceTag: String) { + pageListDialogHelper.onNegativeClickedForBasicDialog( + instanceTag = instanceTag, + editPage = this::editPage + ) + } + + private fun onDeleteConfirmed(pageId: RemoteId) { + launch(defaultDispatcher) { + pageMap[pageId.value]?.let { deletePage(it) } + } + } + + private fun editPage(pageId: RemoteId, loadAutoSaveRevision: LoadAutoSaveRevision = false) { + _editPage.postValue(Pair(pageMap[pageId.value], loadAutoSaveRevision)) + } + private fun isRemotePreviewingFromPostsList() = _previewState.value != null && _previewState.value != PostListRemotePreviewState.NONE diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index d01329ddbfe0..8d9e15459b63 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -358,6 +358,7 @@ Which version would you like to edit? You recently made changes to this post but didn\'t save them. Choose a version to load:\n\n + You recently made changes to this page but didn\'t save them. Choose a version to load:\n\n From this app\nSaved on %s\n\nFrom another device\nSaved on %s\n The version from another device The version from this app diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/PagesViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/PagesViewModelTest.kt index 80caf8820630..0729b9e8ae0f 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/PagesViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/pages/PagesViewModelTest.kt @@ -63,6 +63,8 @@ class PagesViewModelTest { actionPerfomer = actionPerformer, networkUtils = networkUtils, previewStateHelper = mock(), + analyticsTracker = mock(), + pageConflictResolver = mock(), uiDispatcher = Dispatchers.Unconfined, defaultDispatcher = Dispatchers.Unconfined, eventBusWrapper = mock(),