From a3df425ea5c30cd2e1f95b48b6317e87e828584b Mon Sep 17 00:00:00 2001 From: adalpari Date: Wed, 2 Sep 2026 13:24:36 +0200 Subject: [PATCH 01/14] CMM-2376: Add a Featured image option to the pre-publish sheet The publish flow had no featured-image entry point, unlike iOS. Add a "Featured image" row that opens a set/replace/remove sub-screen, reusing the existing featured-image helpers. Co-Authored-By: Claude Opus 4.8 --- .../android/modules/AppComponent.java | 3 + .../android/modules/ViewModelModule.java | 6 + .../android/ui/posts/EditPostActivity.kt | 4 + .../PrepublishingBottomSheetFragment.kt | 9 + .../prepublishing/PrepublishingViewModel.kt | 5 +- .../PrepublishingFeaturedImageFragment.kt | 209 ++++++++++++++++++ .../PrepublishingFeaturedImageViewModel.kt | 98 ++++++++ .../home/PrepublishingHomeItemUiState.kt | 4 + .../home/PrepublishingHomeViewModel.kt | 13 ++ .../prepublishing_featured_image_fragment.xml | 134 +++++++++++ WordPress/src/main/res/values/strings.xml | 4 + ...PrepublishingFeaturedImageViewModelTest.kt | 134 +++++++++++ 12 files changed, 622 insertions(+), 1 deletion(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt create mode 100644 WordPress/src/main/res/layout/prepublishing_featured_image_fragment.xml create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index 7c9d265446b3..13645079eef6 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -99,6 +99,7 @@ import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetFragment; import org.wordpress.android.ui.posts.prepublishing.categories.PrepublishingCategoriesFragment; import org.wordpress.android.ui.posts.prepublishing.categories.addcategory.PrepublishingAddCategoryFragment; +import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageFragment; import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeAdapter; import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeFragment; import org.wordpress.android.ui.posts.EditorLauncher; @@ -456,6 +457,8 @@ public interface AppComponent { void inject(PrepublishingCategoriesFragment object); + void inject(PrepublishingFeaturedImageFragment object); + void inject(PrepublishingAddCategoryFragment object); void inject(PrepublishingSocialFragment object); diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java index b94a64bac61d..e820a76b4956 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java @@ -40,6 +40,7 @@ import org.wordpress.android.ui.posts.prepublishing.PrepublishingViewModel; import org.wordpress.android.ui.posts.prepublishing.categories.PrepublishingCategoriesViewModel; import org.wordpress.android.ui.posts.prepublishing.categories.addcategory.PrepublishingAddCategoryViewModel; +import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel; import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewModel; import org.wordpress.android.ui.posts.prepublishing.publishsettings.PrepublishingPublishSettingsViewModel; import org.wordpress.android.ui.posts.prepublishing.tags.PrepublishingTagsViewModel; @@ -367,6 +368,11 @@ abstract class ViewModelModule { @ViewModelKey(PrepublishingCategoriesViewModel.class) abstract ViewModel prepublishingCategoriesViewModel(PrepublishingCategoriesViewModel viewModel); + @Binds + @IntoMap + @ViewModelKey(PrepublishingFeaturedImageViewModel.class) + abstract ViewModel prepublishingFeaturedImageViewModel(PrepublishingFeaturedImageViewModel viewModel); + @Binds @IntoMap @ViewModelKey(PrepublishingAddCategoryViewModel.class) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index 4d10b46e74e2..be3e835f2135 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -2757,6 +2757,10 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor } } else if (editPostSettingsFragment != null) { editPostSettingsFragment?.updateFeaturedImage(mediaId, imagePicked) + } else { + // The post settings screen isn't shown (e.g. the featured image is being set from the + // pre-publish sheet), so write the id straight to the post ourselves. + updateFeaturedImageUseCase.updateFeaturedImage(mediaId, editPostRepository) { } } if (editorFragment is GutenbergEditorFragment) { (editorFragment as GutenbergEditorFragment).sendToJSFeaturedImageId(mediaId.toInt()) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetFragment.kt index 7323af942cc6..f5cfa67f878f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetFragment.kt @@ -32,12 +32,14 @@ import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel.ActionEvent.O import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel.ActionEvent.OpenSubscribeJetpackSocial import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.ADD_CATEGORY import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.CATEGORIES +import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.FEATURED_IMAGE import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.HOME import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.PUBLISH import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.SOCIAL import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.TAGS import org.wordpress.android.ui.posts.prepublishing.categories.PrepublishingCategoriesFragment import org.wordpress.android.ui.posts.prepublishing.categories.addcategory.PrepublishingAddCategoryFragment +import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageFragment import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeFragment import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType import org.wordpress.android.ui.posts.prepublishing.home.PublishPost @@ -241,6 +243,13 @@ class PrepublishingBottomSheetFragment : WPBottomSheetDialogFragment(), PrepublishingSocialFragment.TAG ) } + + FEATURED_IMAGE -> { + Pair( + PrepublishingFeaturedImageFragment.newInstance(navigationTarget.site), + PrepublishingFeaturedImageFragment.TAG + ) + } } fadeInFragment(fragment, tag) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt index 8ef6138a9f61..c9288af77080 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt @@ -15,6 +15,7 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.TaxonomyStore.OnTaxonomyChanged import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.ADD_CATEGORY import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.CATEGORIES +import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.FEATURED_IMAGE import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.HOME import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.PUBLISH import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType @@ -101,7 +102,8 @@ class PrepublishingViewModel @Inject constructor(private val dispatcher: Dispatc // For the case where this is not a story and hence there's no EditText in the HOME screen, we're ok too, // because there wouldn't have been a keyboard up anyway. if (prepublishingScreen == PUBLISH || - prepublishingScreen == CATEGORIES + prepublishingScreen == CATEGORIES || + prepublishingScreen == FEATURED_IMAGE ) { _dismissKeyboard.postValue(Event(Unit)) } @@ -198,6 +200,7 @@ enum class PrepublishingScreen : Parcelable { CATEGORIES, ADD_CATEGORY, SOCIAL, + FEATURED_IMAGE, } data class PrepublishingNavigationTarget( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt new file mode 100644 index 000000000000..dbe94430723e --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt @@ -0,0 +1,209 @@ +package org.wordpress.android.ui.posts.prepublishing.featuredimage + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.View +import android.widget.ImageView.ScaleType +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode.MAIN +import org.wordpress.android.R +import org.wordpress.android.WordPress +import org.wordpress.android.databinding.PrepublishingFeaturedImageFragmentBinding +import org.wordpress.android.databinding.PrepublishingToolbarBinding +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.MediaStore.OnMediaUploaded +import org.wordpress.android.ui.photopicker.MediaPickerLauncher +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditorDataProvider +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState +import org.wordpress.android.ui.posts.prepublishing.PrepublishingViewModel +import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel.UiState +import org.wordpress.android.ui.posts.prepublishing.listeners.PrepublishingScreenClosedListener +import org.wordpress.android.ui.utils.UiHelpers +import org.wordpress.android.util.extensions.getSerializableCompat +import org.wordpress.android.util.image.ImageManager +import org.wordpress.android.util.image.ImageManager.RequestListener +import org.wordpress.android.util.image.ImageType +import org.wordpress.android.viewmodel.observeEvent +import javax.inject.Inject + +class PrepublishingFeaturedImageFragment : Fragment(R.layout.prepublishing_featured_image_fragment) { + private var closeListener: PrepublishingScreenClosedListener? = null + + @Inject + lateinit var viewModelFactory: ViewModelProvider.Factory + + @Inject + lateinit var uiHelpers: UiHelpers + + @Inject + lateinit var imageManager: ImageManager + + @Inject + lateinit var mediaPickerLauncher: MediaPickerLauncher + + @Inject + lateinit var dispatcher: Dispatcher + + private lateinit var viewModel: PrepublishingFeaturedImageViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + (requireActivity().application as WordPress).component().inject(this) + } + + override fun onStart() { + super.onStart() + dispatcher.register(this) + } + + override fun onStop() { + dispatcher.unregister(this) + super.onStop() + } + + override fun onAttach(context: Context) { + super.onAttach(context) + closeListener = parentFragment as PrepublishingScreenClosedListener + } + + override fun onDetach() { + super.onDetach() + closeListener = null + } + + override fun onResume() { + super.onResume() + // The media picker returns its result to the host activity, so refresh here to reflect + // any image that was just set or queued for upload while this screen was in the background. + if (::viewModel.isInitialized) { + viewModel.refreshFeaturedImageState() + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + with(PrepublishingFeaturedImageFragmentBinding.bind(view)) { + includePrepublishingToolbar.initToolbar() + initClickListeners() + initViewModel() + } + } + + private fun PrepublishingToolbarBinding.initToolbar() { + toolbarTitle.text = getString(R.string.prepublishing_nudges_toolbar_title_featured_image) + backButton.setOnClickListener { viewModel.onBackButtonClick() } + } + + private fun PrepublishingFeaturedImageFragmentBinding.initClickListeners() { + buttonSetFeaturedImage.setOnClickListener { viewModel.onSetOrChangeClicked() } + buttonRemoveFeaturedImage.setOnClickListener { viewModel.onRemoveClicked() } + featuredImageRetryOverlay.setOnClickListener { viewModel.onRetryClicked() } + } + + private fun PrepublishingFeaturedImageFragmentBinding.initViewModel() { + viewModel = ViewModelProvider( + this@PrepublishingFeaturedImageFragment, + viewModelFactory + )[PrepublishingFeaturedImageViewModel::class.java] + + viewModel.uiState.observe(viewLifecycleOwner) { uiState -> + renderUiState(uiState) + } + + viewModel.navigateToHomeScreen.observeEvent(viewLifecycleOwner) { + closeListener?.onBackClicked() + } + + viewModel.launchFeaturedImagePicker.observeEvent(viewLifecycleOwner) { postId -> + mediaPickerLauncher.showFeaturedImagePicker(requireActivity(), getSite(), postId) + } + + // updateAsync mutates the shared post and fires postChanged, so this catches an image set + // from the media library, an upload that just completed, and a removal. + getEditPostRepository().postChanged.observeEvent(viewLifecycleOwner) { + viewModel.refreshFeaturedImageState() + } + + viewModel.start(getEditPostRepository(), getSite()) + } + + private fun PrepublishingFeaturedImageFragmentBinding.renderUiState(uiState: UiState) { + val state = uiState.featuredImageData.uiState + val hasImage = state != FeaturedImageState.IMAGE_EMPTY + with(uiHelpers) { + updateVisibility(featuredImageEmptyText, !hasImage) + updateVisibility(postFeaturedImageContainer, hasImage) + updateVisibility(postFeaturedImage, state.imageViewVisible) + updateVisibility(postFeaturedImageLocal, state.localImageViewVisible) + updateVisibility(featuredImageRetryOverlay, state.retryOverlayVisible) + updateVisibility(featuredImageProgressOverlay, state.progressOverlayVisible) + updateVisibility(buttonRemoveFeaturedImage, uiState.isRemoveButtonVisible) + } + buttonSetFeaturedImage.text = uiHelpers.getTextOfUiString(requireContext(), uiState.setButtonText) + if (!state.localImageViewVisible) { + imageManager.cancelRequestAndClearImageView(postFeaturedImageLocal) + } + loadFeaturedImage(uiState) + } + + private fun PrepublishingFeaturedImageFragmentBinding.loadFeaturedImage(uiState: UiState) { + val mediaUri = uiState.featuredImageData.mediaUri ?: return + if (uiState.featuredImageData.uiState == FeaturedImageState.REMOTE_IMAGE_LOADING) { + // Keep the local image (if any) visible until the remote image is ready to avoid a flash + // of an empty view when the local image is swapped for the remote one. + imageManager.loadWithResultListener( + postFeaturedImage, ImageType.IMAGE, mediaUri, ScaleType.FIT_CENTER, null, + object : RequestListener { + override fun onLoadFailed(e: Exception?, model: Any?) {} + override fun onResourceReady(resource: Drawable, model: Any?) { + with(uiHelpers) { + updateVisibility(postFeaturedImage, true) + updateVisibility(postFeaturedImageLocal, false) + } + } + } + ) + } else { + imageManager.load(postFeaturedImageLocal, ImageType.IMAGE, mediaUri, ScaleType.FIT_CENTER) + } + } + + private fun getSite(): SiteModel = + requireNotNull(arguments?.getSerializableCompat(WordPress.SITE)) + + private fun getEditPostRepository(): EditPostRepository { + val activity = activity + val editorDataProvider = if (activity is EditorDataProvider) { + activity + } else { + throw RuntimeException("$activity must implement EditorDataProvider") + } + return editorDataProvider.editPostRepository + } + + @Suppress("unused") + @Subscribe(threadMode = MAIN) + fun onMediaUploaded(event: OnMediaUploaded) { + val media = event.media + if (media != null && media.markedLocallyAsFeatured) { + viewModel.refreshFeaturedImageState() + } + } + + companion object { + const val TAG = "prepublishing_featured_image_fragment_tag" + + @JvmStatic + fun newInstance(site: SiteModel): PrepublishingFeaturedImageFragment { + val bundle = Bundle().apply { + putSerializable(WordPress.SITE, site) + } + return PrepublishingFeaturedImageFragment().apply { arguments = bundle } + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt new file mode 100644 index 000000000000..8b55d47a1daa --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt @@ -0,0 +1,98 @@ +package org.wordpress.android.ui.posts.prepublishing.featuredimage + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.CoroutineDispatcher +import org.wordpress.android.R +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.modules.BG_THREAD +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.FeaturedImageHelper +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState +import org.wordpress.android.ui.posts.FeaturedImageHelper.TrackableEvent +import org.wordpress.android.ui.posts.UpdateFeaturedImageUseCase +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.viewmodel.Event +import org.wordpress.android.viewmodel.ScopedViewModel +import javax.inject.Inject +import javax.inject.Named + +class PrepublishingFeaturedImageViewModel @Inject constructor( + private val featuredImageHelper: FeaturedImageHelper, + private val updateFeaturedImageUseCase: UpdateFeaturedImageUseCase, + @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher +) : ScopedViewModel(bgDispatcher) { + private var isStarted = false + private lateinit var editPostRepository: EditPostRepository + private lateinit var siteModel: SiteModel + + private val _uiState = MutableLiveData() + val uiState: LiveData = _uiState + + private val _navigateToHomeScreen = MutableLiveData>() + val navigateToHomeScreen: LiveData> = _navigateToHomeScreen + + private val _launchFeaturedImagePicker = MutableLiveData>() + val launchFeaturedImagePicker: LiveData> = _launchFeaturedImagePicker + + fun start(editPostRepository: EditPostRepository, siteModel: SiteModel) { + this.editPostRepository = editPostRepository + this.siteModel = siteModel + if (isStarted) return + isStarted = true + refreshFeaturedImageState() + } + + fun onSetOrChangeClicked() { + val postId = editPostRepository.getPost()?.id ?: return + featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_SET_CLICKED, postId) + _launchFeaturedImagePicker.postValue(Event(postId)) + } + + fun onRemoveClicked() { + val post = editPostRepository.getPost() ?: return + featuredImageHelper.cancelFeaturedImageUpload(siteModel, post, false) + featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_REMOVE_CLICKED, post.id) + updateFeaturedImageUseCase.updateFeaturedImage(NO_FEATURED_IMAGE_ID, editPostRepository) { + refreshFeaturedImageState() + } + } + + fun onRetryClicked() { + val post = editPostRepository.getPost() ?: return + featuredImageHelper.retryFeaturedImageUpload(siteModel, post) + refreshFeaturedImageState() + } + + fun onBackButtonClick() { + _navigateToHomeScreen.postValue(Event(Unit)) + } + + fun refreshFeaturedImageState() { + val post = editPostRepository.getPost() ?: return + val featuredImageData = featuredImageHelper.createCurrentFeaturedImageState(siteModel, post) + val hasImage = featuredImageData.uiState != FeaturedImageState.IMAGE_EMPTY + _uiState.postValue( + UiState( + featuredImageData = featuredImageData, + setButtonText = if (hasImage) { + UiStringRes(R.string.post_settings_choose_featured_image) + } else { + UiStringRes(R.string.post_settings_set_featured_image) + }, + isRemoveButtonVisible = hasImage + ) + ) + } + + data class UiState( + val featuredImageData: FeaturedImageData, + val setButtonText: UiStringRes, + val isRemoveButtonVisible: Boolean + ) + + companion object { + private const val NO_FEATURED_IMAGE_ID = 0L + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeItemUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeItemUiState.kt index 080079c47ef6..a22924fa8a64 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeItemUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeItemUiState.kt @@ -99,6 +99,10 @@ sealed class PrepublishingHomeItemUiState( UiStringRes(R.string.prepublishing_nudges_categories_action), PrepublishingScreen.CATEGORIES, ) + object FeaturedImage : PrepublishingScreenNavigation( + UiStringRes(R.string.prepublishing_nudges_featured_image_action), + PrepublishingScreen.FEATURED_IMAGE, + ) object AddCategory : PrepublishingScreenNavigation( UiStringRes(R.string.prepublishing_nudges_categories_action), PrepublishingScreen.ADD_CATEGORY, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt index 87dbea59e99b..cbc31f55cf8e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt @@ -123,6 +123,19 @@ class PrepublishingHomeViewModel @Inject constructor( )) } + if (!editPostRepository.isPage) { + add(HomeUiState( + navigationAction = PrepublishingScreenNavigation.FeaturedImage, + actionResult = if (editPostRepository.featuredImageId != 0L) { + UiStringRes(R.string.prepublishing_nudges_home_featured_image_set) + } else { + UiStringRes(R.string.prepublishing_nudges_home_featured_image_not_set) + }, + actionClickable = true, + onNavigationActionClicked = ::onActionClicked + )) + } + add(SocialUiState.Hidden) add(getButtonUiStateUseCase.getUiState(editPostRepository, site) { publishPost -> diff --git a/WordPress/src/main/res/layout/prepublishing_featured_image_fragment.xml b/WordPress/src/main/res/layout/prepublishing_featured_image_fragment.xml new file mode 100644 index 000000000000..57c431e66df8 --- /dev/null +++ b/WordPress/src/main/res/layout/prepublishing_featured_image_fragment.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 90e9c9afba18..3593230d4dc7 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -3821,6 +3821,10 @@ Not set Not set Categories + Featured image + Featured image + Set + Not set Add New Category Add Category Social sharing diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt new file mode 100644 index 000000000000..79e633d2693d --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt @@ -0,0 +1,134 @@ +package org.wordpress.android.ui.posts.prepublishing + +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.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.R +import org.wordpress.android.fluxc.model.PostImmutableModel +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.ui.posts.EditPostRepository +import org.wordpress.android.ui.posts.FeaturedImageHelper +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState +import org.wordpress.android.ui.posts.FeaturedImageHelper.TrackableEvent +import org.wordpress.android.ui.posts.UpdateFeaturedImageUseCase +import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel +import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel.UiState +import org.wordpress.android.ui.utils.UiString.UiStringRes +import org.wordpress.android.viewmodel.Event + +private const val POST_ID = 1 + +@ExperimentalCoroutinesApi +class PrepublishingFeaturedImageViewModelTest : BaseUnitTest() { + private lateinit var viewModel: PrepublishingFeaturedImageViewModel + + @Mock + lateinit var featuredImageHelper: FeaturedImageHelper + + @Mock + lateinit var updateFeaturedImageUseCase: UpdateFeaturedImageUseCase + + @Mock + lateinit var editPostRepository: EditPostRepository + + @Mock + lateinit var post: PostImmutableModel + + private val site: SiteModel = SiteModel() + + @Before + fun setup() { + viewModel = PrepublishingFeaturedImageViewModel( + featuredImageHelper, + updateFeaturedImageUseCase, + testDispatcher() + ) + whenever(editPostRepository.getPost()).thenReturn(post) + whenever(post.id).thenReturn(POST_ID) + stubFeaturedImageState(FeaturedImageState.IMAGE_EMPTY) + } + + private fun stubFeaturedImageState(state: FeaturedImageState) { + whenever(featuredImageHelper.createCurrentFeaturedImageState(site, post)) + .thenReturn(FeaturedImageData(state, null)) + } + + @Test + fun `when started with no featured image the remove button is hidden and set button shows set text`() { + var uiState: UiState? = null + viewModel.uiState.observeForever { uiState = it } + + viewModel.start(editPostRepository, site) + + assertThat(uiState?.isRemoveButtonVisible).isFalse + assertThat((uiState?.setButtonText as UiStringRes).stringRes) + .isEqualTo(R.string.post_settings_set_featured_image) + } + + @Test + fun `when started with a featured image the remove button is shown and set button shows change text`() { + stubFeaturedImageState(FeaturedImageState.REMOTE_IMAGE_LOADING) + var uiState: UiState? = null + viewModel.uiState.observeForever { uiState = it } + + viewModel.start(editPostRepository, site) + + assertThat(uiState?.isRemoveButtonVisible).isTrue + assertThat((uiState?.setButtonText as UiStringRes).stringRes) + .isEqualTo(R.string.post_settings_choose_featured_image) + } + + @Test + fun `when set or change is clicked the featured image picker is launched with the post id`() { + var event: Event? = null + viewModel.launchFeaturedImagePicker.observeForever { event = it } + + viewModel.start(editPostRepository, site) + viewModel.onSetOrChangeClicked() + + assertThat(event?.peekContent()).isEqualTo(POST_ID) + verify(featuredImageHelper).trackFeaturedImageEvent(TrackableEvent.IMAGE_SET_CLICKED, POST_ID) + } + + @Test + fun `when remove is clicked the featured image is cleared and pending upload is cancelled`() { + viewModel.start(editPostRepository, site) + viewModel.onRemoveClicked() + + verify(featuredImageHelper).cancelFeaturedImageUpload(site, post, false) + verify(updateFeaturedImageUseCase).updateFeaturedImage(eq(0L), eq(editPostRepository), any()) + } + + @Test + fun `when set or change is clicked without a post the picker is not launched`() { + var event: Event? = null + viewModel.launchFeaturedImagePicker.observeForever { event = it } + + viewModel.start(editPostRepository, site) + whenever(editPostRepository.getPost()).thenReturn(null) + viewModel.onSetOrChangeClicked() + + assertThat(event).isNull() + verify(featuredImageHelper, never()).trackFeaturedImageEvent(any(), any()) + } + + @Test + fun `when back is clicked navigateToHomeScreen is triggered`() { + var event: Event? = null + viewModel.navigateToHomeScreen.observeForever { event = it } + + viewModel.onBackButtonClick() + + assertThat(event).isNotNull + } +} From 12ac6c4cd2cf5ab23628d70f10f86391f58cb509 Mon Sep 17 00:00:00 2001 From: adalpari Date: Wed, 2 Sep 2026 13:39:45 +0200 Subject: [PATCH 02/14] CMM-2376: Fix featured image preview not rendering until reopen Observe postChanged raw instead of via observeEvent, since it is a single-consumption Event shared with other observers that would swallow the refresh. Also render only one image view per state so an empty local view can't cover the loaded remote image. Co-Authored-By: Claude Opus 4.8 --- .../PrepublishingFeaturedImageFragment.kt | 55 +++++++++---------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt index dbe94430723e..776eb18a126d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt @@ -1,7 +1,6 @@ package org.wordpress.android.ui.posts.prepublishing.featuredimage import android.content.Context -import android.graphics.drawable.Drawable import android.os.Bundle import android.view.View import android.widget.ImageView.ScaleType @@ -26,7 +25,6 @@ import org.wordpress.android.ui.posts.prepublishing.listeners.PrepublishingScree import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.extensions.getSerializableCompat import org.wordpress.android.util.image.ImageManager -import org.wordpress.android.util.image.ImageManager.RequestListener import org.wordpress.android.util.image.ImageType import org.wordpress.android.viewmodel.observeEvent import javax.inject.Inject @@ -124,8 +122,10 @@ class PrepublishingFeaturedImageFragment : Fragment(R.layout.prepublishing_featu } // updateAsync mutates the shared post and fires postChanged, so this catches an image set - // from the media library, an upload that just completed, and a removal. - getEditPostRepository().postChanged.observeEvent(viewLifecycleOwner) { + // from the media library, an upload that just completed, and a removal. Observe it raw + // (not observeEvent): postChanged is a single-consumption Event with other observers, so + // observeEvent would miss it whenever another observer handles it first. + getEditPostRepository().postChanged.observe(viewLifecycleOwner) { viewModel.refreshFeaturedImageState() } @@ -134,42 +134,37 @@ class PrepublishingFeaturedImageFragment : Fragment(R.layout.prepublishing_featu private fun PrepublishingFeaturedImageFragmentBinding.renderUiState(uiState: UiState) { val state = uiState.featuredImageData.uiState + // A remote image (already on the server) uses postFeaturedImage; a local image being + // uploaded uses postFeaturedImageLocal. Only ever show one so an empty view can't cover the + // other in the FrameLayout. + val isRemote = state == FeaturedImageState.REMOTE_IMAGE_LOADING || + state == FeaturedImageState.REMOTE_IMAGE_SET + val isLocalUpload = state == FeaturedImageState.IMAGE_UPLOAD_IN_PROGRESS || + state == FeaturedImageState.IMAGE_UPLOAD_FAILED val hasImage = state != FeaturedImageState.IMAGE_EMPTY with(uiHelpers) { updateVisibility(featuredImageEmptyText, !hasImage) updateVisibility(postFeaturedImageContainer, hasImage) - updateVisibility(postFeaturedImage, state.imageViewVisible) - updateVisibility(postFeaturedImageLocal, state.localImageViewVisible) + updateVisibility(postFeaturedImage, isRemote) + updateVisibility(postFeaturedImageLocal, isLocalUpload) updateVisibility(featuredImageRetryOverlay, state.retryOverlayVisible) updateVisibility(featuredImageProgressOverlay, state.progressOverlayVisible) updateVisibility(buttonRemoveFeaturedImage, uiState.isRemoveButtonVisible) } buttonSetFeaturedImage.text = uiHelpers.getTextOfUiString(requireContext(), uiState.setButtonText) - if (!state.localImageViewVisible) { - imageManager.cancelRequestAndClearImageView(postFeaturedImageLocal) - } - loadFeaturedImage(uiState) - } - private fun PrepublishingFeaturedImageFragmentBinding.loadFeaturedImage(uiState: UiState) { - val mediaUri = uiState.featuredImageData.mediaUri ?: return - if (uiState.featuredImageData.uiState == FeaturedImageState.REMOTE_IMAGE_LOADING) { - // Keep the local image (if any) visible until the remote image is ready to avoid a flash - // of an empty view when the local image is swapped for the remote one. - imageManager.loadWithResultListener( - postFeaturedImage, ImageType.IMAGE, mediaUri, ScaleType.FIT_CENTER, null, - object : RequestListener { - override fun onLoadFailed(e: Exception?, model: Any?) {} - override fun onResourceReady(resource: Drawable, model: Any?) { - with(uiHelpers) { - updateVisibility(postFeaturedImage, true) - updateVisibility(postFeaturedImageLocal, false) - } - } - } - ) - } else { - imageManager.load(postFeaturedImageLocal, ImageType.IMAGE, mediaUri, ScaleType.FIT_CENTER) + val mediaUri = uiState.featuredImageData.mediaUri + when { + isRemote && !mediaUri.isNullOrEmpty() -> { + imageManager.cancelRequestAndClearImageView(postFeaturedImageLocal) + imageManager.load(postFeaturedImage, ImageType.IMAGE, mediaUri, ScaleType.FIT_CENTER) + } + isLocalUpload && !mediaUri.isNullOrEmpty() -> + imageManager.load(postFeaturedImageLocal, ImageType.IMAGE, mediaUri, ScaleType.FIT_CENTER) + else -> { + imageManager.cancelRequestAndClearImageView(postFeaturedImage) + imageManager.cancelRequestAndClearImageView(postFeaturedImageLocal) + } } } From e7c906cd6b4a9f4d4f4c8b655cb1277ed16fadac Mon Sep 17 00:00:00 2001 From: adalpari Date: Wed, 2 Sep 2026 14:10:36 +0200 Subject: [PATCH 03/14] CMM-2376: Sync editor featured image on removal; review + codecheck fixes Route featured-image removal through a new EditorDataProvider hook so the editor's cached featured image is cleared (set already synced via the picker result path). Add featured-image row coverage to the home ViewModel test and resolve detekt findings. Co-Authored-By: Claude Opus 4.8 --- .../android/ui/posts/EditPostActivity.kt | 3 +++ .../ui/posts/EditPostSettingsFragment.java | 6 +++++ .../android/ui/posts/GutenbergKitActivity.kt | 3 +++ .../android/ui/posts/PostsListActivity.kt | 3 +++ .../PrepublishingFeaturedImageFragment.kt | 17 ++++++++---- .../PrepublishingFeaturedImageViewModel.kt | 6 +++++ .../posts/PrepublishingHomeViewModelTest.kt | 26 ++++++++++++++++++- ...PrepublishingFeaturedImageViewModelTest.kt | 17 +++++++++++- 8 files changed, 74 insertions(+), 7 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index be3e835f2135..7bb978f93ea5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -3886,6 +3886,9 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor // EditorDataProvider methods override fun getEditPostRepository() = editPostRepository override fun getSite() = siteModel + override fun syncFeaturedImageIdToEditor() { + editorFragment?.setFeaturedImageId(editPostRepository.featuredImageId) + } override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { // This is a workaround for bag discovered on Chromebooks, where Enter key will not work in the toolbar menu diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java index 777a82ebe5d3..60d01196a5e2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java @@ -175,6 +175,12 @@ public interface EditorDataProvider { EditPostRepository getEditPostRepository(); SiteModel getSite(); + + /** + * Pushes the post's current featured image id to the editor. Used by the pre-publish sheet + * after it changes the featured image, so the editor doesn't keep showing a stale image. + */ + void syncFeaturedImageIdToEditor(); } public static EditPostSettingsFragment newInstance() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt index 66ce7535fbcc..49a4e85a2fb5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt @@ -3129,6 +3129,9 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene // EditorDataProvider methods override fun getEditPostRepository() = editPostRepository override fun getSite() = siteModel + override fun syncFeaturedImageIdToEditor() { + editorFragment?.setFeaturedImageId(editPostRepository.featuredImageId) + } override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { // This is a workaround for bag discovered on Chromebooks, where Enter key will not work in the toolbar menu diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index f7fad74cf6f2..0d9753996c3c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -128,6 +128,9 @@ class PostsListActivity : BaseAppCompatActivity(), override fun getSite() = site override fun getEditPostRepository() = editPostRepository + override fun syncFeaturedImageIdToEditor() { + // No editor is hosted here, so there is nothing to keep in sync. + } private lateinit var viewModel: PostListMainViewModel diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt index 776eb18a126d..e7c70594c388 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt @@ -19,7 +19,6 @@ import org.wordpress.android.ui.photopicker.MediaPickerLauncher import org.wordpress.android.ui.posts.EditPostRepository import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditorDataProvider import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState -import org.wordpress.android.ui.posts.prepublishing.PrepublishingViewModel import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel.UiState import org.wordpress.android.ui.posts.prepublishing.listeners.PrepublishingScreenClosedListener import org.wordpress.android.ui.utils.UiHelpers @@ -121,6 +120,10 @@ class PrepublishingFeaturedImageFragment : Fragment(R.layout.prepublishing_featu mediaPickerLauncher.showFeaturedImagePicker(requireActivity(), getSite(), postId) } + viewModel.syncFeaturedImageToEditor.observeEvent(viewLifecycleOwner) { + getEditorDataProvider().syncFeaturedImageIdToEditor() + } + // updateAsync mutates the shared post and fires postChanged, so this catches an image set // from the media library, an upload that just completed, and a removal. Observe it raw // (not observeEvent): postChanged is a single-consumption Event with other observers, so @@ -132,6 +135,9 @@ class PrepublishingFeaturedImageFragment : Fragment(R.layout.prepublishing_featu viewModel.start(getEditPostRepository(), getSite()) } + // TODO CMM-2376 follow-up: this featured-image rendering (state->visibility, overlays, remote vs + // local image) mirrors EditPostSettingsFragment.updateFeaturedImageView. Extract a shared + // component so the two don't drift. private fun PrepublishingFeaturedImageFragmentBinding.renderUiState(uiState: UiState) { val state = uiState.featuredImageData.uiState // A remote image (already on the server) uses postFeaturedImage; a local image being @@ -171,14 +177,15 @@ class PrepublishingFeaturedImageFragment : Fragment(R.layout.prepublishing_featu private fun getSite(): SiteModel = requireNotNull(arguments?.getSerializableCompat(WordPress.SITE)) - private fun getEditPostRepository(): EditPostRepository { + private fun getEditPostRepository(): EditPostRepository = getEditorDataProvider().editPostRepository + + private fun getEditorDataProvider(): EditorDataProvider { val activity = activity - val editorDataProvider = if (activity is EditorDataProvider) { + return if (activity is EditorDataProvider) { activity } else { - throw RuntimeException("$activity must implement EditorDataProvider") + error("$activity must implement EditorDataProvider") } - return editorDataProvider.editPostRepository } @Suppress("unused") diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt index 8b55d47a1daa..1fb9eb591604 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt @@ -36,6 +36,9 @@ class PrepublishingFeaturedImageViewModel @Inject constructor( private val _launchFeaturedImagePicker = MutableLiveData>() val launchFeaturedImagePicker: LiveData> = _launchFeaturedImagePicker + private val _syncFeaturedImageToEditor = MutableLiveData>() + val syncFeaturedImageToEditor: LiveData> = _syncFeaturedImageToEditor + fun start(editPostRepository: EditPostRepository, siteModel: SiteModel) { this.editPostRepository = editPostRepository this.siteModel = siteModel @@ -56,6 +59,9 @@ class PrepublishingFeaturedImageViewModel @Inject constructor( featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_REMOVE_CLICKED, post.id) updateFeaturedImageUseCase.updateFeaturedImage(NO_FEATURED_IMAGE_ID, editPostRepository) { refreshFeaturedImageState() + // The model is the source of truth, but the editor caches the featured image separately, + // so tell the host to push the now-cleared id to the editor. + _syncFeaturedImageToEditor.postValue(Event(Unit)) } } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt index 6542b486233f..1155e102353c 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt @@ -84,7 +84,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { @Test fun `verify that post home actions are propagated to prepublishingHomeUiState once the viewModel is started`() { // arrange - val expectedActionsAmount = 3 + val expectedActionsAmount = 4 // act viewModel.start(editPostRepository, site) @@ -158,6 +158,30 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { assertThat(getHomeUiState(PrepublishingScreenNavigation.Categories)).isNull() } + @Test + fun `verify that featured image action is propagated to prepublishingHomeUiState once post is not a page`() { + // arrange + whenever(editPostRepository.isPage).thenReturn(false) + + // act + viewModel.start(editPostRepository, site) + + // assert + assertThat(getHomeUiState(PrepublishingScreenNavigation.FeaturedImage)).isNotNull() + } + + @Test + fun `verify that featured image action is not propagated to prepublishingHomeUiState once post is a page`() { + // arrange + whenever(editPostRepository.isPage).thenReturn(true) + + // act + viewModel.start(editPostRepository, site) + + // assert + assertThat(getHomeUiState(PrepublishingScreenNavigation.FeaturedImage)).isNull() + } + @Test fun `verify that header ui state is propagated to prepublishingHomeUiState once the viewModel is started`() { // arrange diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt index 79e633d2693d..f0752af4e023 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt @@ -7,7 +7,6 @@ import org.junit.Test import org.mockito.Mock import org.mockito.kotlin.any import org.mockito.kotlin.eq -import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -109,6 +108,22 @@ class PrepublishingFeaturedImageViewModelTest : BaseUnitTest() { verify(updateFeaturedImageUseCase).updateFeaturedImage(eq(0L), eq(editPostRepository), any()) } + @Test + fun `when remove completes the editor is asked to sync`() { + whenever(updateFeaturedImageUseCase.updateFeaturedImage(eq(0L), eq(editPostRepository), any())) + .thenAnswer { invocation -> + @Suppress("UNCHECKED_CAST") + (invocation.arguments[2] as (PostImmutableModel) -> Unit).invoke(post) + } + var event: Event? = null + viewModel.syncFeaturedImageToEditor.observeForever { event = it } + + viewModel.start(editPostRepository, site) + viewModel.onRemoveClicked() + + assertThat(event).isNotNull + } + @Test fun `when set or change is clicked without a post the picker is not launched`() { var event: Event? = null From 3c727469404035c1d9ceb2f6d98e47e6bbb17f39 Mon Sep 17 00:00:00 2001 From: adalpari Date: Wed, 2 Sep 2026 16:04:17 +0200 Subject: [PATCH 04/14] CMM-2376: Wire featured image across pre-publish hosts; review fixes Address code-review findings on the featured-image pre-publish option: - Gate the row to hosts that can fully edit a featured image via a new EditorDataProvider.supportsFeaturedImageEditing(); the posts-list quick-publish flow has no editor, so it no longer offers a Set button that can't complete. - Fix GutenbergKitActivity.setFeaturedImageId to write the id and sync the editor when the settings screen isn't shown (sheet is open). - Cancel any pending upload when replacing an image, matching the legacy choose path. - Base the home row's Set/Not set label on the featured image state so an in-progress upload no longer reads as "Not set". Co-Authored-By: Claude Opus 4.8 --- .../android/ui/posts/EditPostActivity.kt | 1 + .../ui/posts/EditPostSettingsFragment.java | 7 ++++++ .../android/ui/posts/GutenbergKitActivity.kt | 7 ++++++ .../android/ui/posts/PostsListActivity.kt | 2 ++ .../PrepublishingFeaturedImageViewModel.kt | 8 ++++--- .../home/PrepublishingHomeFragment.kt | 5 +++- .../home/PrepublishingHomeViewModel.kt | 23 ++++++++++++++++--- .../posts/PrepublishingHomeViewModelTest.kt | 21 +++++++++++++++++ ...PrepublishingFeaturedImageViewModelTest.kt | 1 + 9 files changed, 68 insertions(+), 7 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index 7bb978f93ea5..f9408772d1c7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -3889,6 +3889,7 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor override fun syncFeaturedImageIdToEditor() { editorFragment?.setFeaturedImageId(editPostRepository.featuredImageId) } + override fun supportsFeaturedImageEditing() = true override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { // This is a workaround for bag discovered on Chromebooks, where Enter key will not work in the toolbar menu diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java index 60d01196a5e2..a69e99b6e7c5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostSettingsFragment.java @@ -181,6 +181,13 @@ public interface EditorDataProvider { * after it changes the featured image, so the editor doesn't keep showing a stale image. */ void syncFeaturedImageIdToEditor(); + + /** + * Whether this host can fully edit a featured image (pick, upload, sync to an editor). + * Hosts without an editor (e.g. the posts list quick-publish flow) return false so the + * pre-publish sheet doesn't offer a featured-image option it can't complete. + */ + boolean supportsFeaturedImageEditing(); } public static EditPostSettingsFragment newInstance() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt index 49a4e85a2fb5..2a8eb82a1d09 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt @@ -2473,6 +2473,12 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene } } else if (editPostSettingsFragment != null) { editPostSettingsFragment?.updateFeaturedImage(mediaId, imagePicked) + } else { + // The post settings screen isn't shown (e.g. the featured image is being set from the + // pre-publish sheet), so write the id ourselves and push it to the editor. + updateFeaturedImageUseCase.updateFeaturedImage(mediaId, editPostRepository) { + editorFragment?.setFeaturedImageId(editPostRepository.featuredImageId) + } } } @@ -3132,6 +3138,7 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene override fun syncFeaturedImageIdToEditor() { editorFragment?.setFeaturedImageId(editPostRepository.featuredImageId) } + override fun supportsFeaturedImageEditing() = true override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { // This is a workaround for bag discovered on Chromebooks, where Enter key will not work in the toolbar menu diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt index 0d9753996c3c..f054a95f10c1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/PostsListActivity.kt @@ -131,6 +131,8 @@ class PostsListActivity : BaseAppCompatActivity(), override fun syncFeaturedImageIdToEditor() { // No editor is hosted here, so there is nothing to keep in sync. } + // The quick-publish flow has no editor, so it can't fully support featured-image editing. + override fun supportsFeaturedImageEditing() = false private lateinit var viewModel: PostListMainViewModel diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt index 1fb9eb591604..06d9381262c0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt @@ -48,9 +48,11 @@ class PrepublishingFeaturedImageViewModel @Inject constructor( } fun onSetOrChangeClicked() { - val postId = editPostRepository.getPost()?.id ?: return - featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_SET_CLICKED, postId) - _launchFeaturedImagePicker.postValue(Event(postId)) + val post = editPostRepository.getPost() ?: return + // Replacing an image mid-upload would otherwise leave two uploads marked as featured. + featuredImageHelper.cancelFeaturedImageUpload(siteModel, post, false) + featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_SET_CLICKED, post.id) + _launchFeaturedImagePicker.postValue(Event(post.id)) } fun onRemoveClicked() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt index ea89d63f7125..5ebf60482da0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt @@ -102,9 +102,12 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag actionClickedListener?.onSubmitButtonClicked(publishPost) } - viewModel.start(getEditPostRepository(), getSite()) + viewModel.start(getEditPostRepository(), getSite(), isFeaturedImageEditingSupported()) } + private fun isFeaturedImageEditingSupported(): Boolean = + getEditorDataProvider()?.supportsFeaturedImageEditing() ?: false + private fun setupJetpackSocialViewModel() { jetpackSocialViewModel = (parentFragment as PrepublishingSocialViewModelProvider) .getEditorJetpackSocialViewModel() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt index cbc31f55cf8e..67dedf548700 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt @@ -11,6 +11,8 @@ import org.wordpress.android.fluxc.model.post.PostStatus import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.posts.EditPostRepository import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel +import org.wordpress.android.ui.posts.FeaturedImageHelper +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState import org.wordpress.android.ui.posts.GetCategoriesUseCase import org.wordpress.android.ui.posts.GetPostTagsUseCase import org.wordpress.android.ui.posts.PostSettingsUtils @@ -38,11 +40,13 @@ class PrepublishingHomeViewModel @Inject constructor( private val getButtonUiStateUseCase: GetButtonUiStateUseCase, private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, private val getCategoriesUseCase: GetCategoriesUseCase, + private val featuredImageHelper: FeaturedImageHelper, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher ) : ScopedViewModel(bgDispatcher) { private var isStarted = false private var updateStoryTitleJob: Job? = null private lateinit var editPostRepository: EditPostRepository + private var isFeaturedImageEditingSupported = false private val _onActionClicked = MutableLiveData>() val onActionClicked: LiveData> = _onActionClicked @@ -66,8 +70,13 @@ class PrepublishingHomeViewModel @Inject constructor( } } - fun start(editPostRepository: EditPostRepository, site: SiteModel) { + fun start( + editPostRepository: EditPostRepository, + site: SiteModel, + isFeaturedImageEditingSupported: Boolean = true + ) { this.editPostRepository = editPostRepository + this.isFeaturedImageEditingSupported = isFeaturedImageEditingSupported if (isStarted) return // PrepublishingViewModel already dismisses the sheet when the host has no post, but that // dismissal is asynchronous and the FragmentManager can restore this fragment and create its @@ -123,10 +132,10 @@ class PrepublishingHomeViewModel @Inject constructor( )) } - if (!editPostRepository.isPage) { + if (!editPostRepository.isPage && isFeaturedImageEditingSupported) { add(HomeUiState( navigationAction = PrepublishingScreenNavigation.FeaturedImage, - actionResult = if (editPostRepository.featuredImageId != 0L) { + actionResult = if (hasFeaturedImage(editPostRepository, site)) { UiStringRes(R.string.prepublishing_nudges_home_featured_image_set) } else { UiStringRes(R.string.prepublishing_nudges_home_featured_image_not_set) @@ -225,6 +234,14 @@ class PrepublishingHomeViewModel @Inject constructor( updateStoryTitleJob?.cancel() } + // Reflects an in-progress upload as "set" too, rather than checking the raw id, so a device + // image that is still uploading doesn't briefly read as "Not set". + private fun hasFeaturedImage(editPostRepository: EditPostRepository, site: SiteModel): Boolean { + val post = editPostRepository.getPost() ?: return false + return featuredImageHelper.createCurrentFeaturedImageState(site, post).uiState != + FeaturedImageState.IMAGE_EMPTY + } + private fun onActionClicked(actionType: ActionType) { _onActionClicked.postValue(Event(actionType)) } diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt index 1155e102353c..0c612c609ba1 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt @@ -14,6 +14,8 @@ import org.wordpress.android.R import org.wordpress.android.fluxc.model.PostModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState import org.wordpress.android.fluxc.model.post.PostStatus.PRIVATE import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType.PrepublishingScreenNavigation @@ -48,6 +50,9 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { @Mock lateinit var getCategoriesUseCase: GetCategoriesUseCase + @Mock + lateinit var featuredImageHelper: FeaturedImageHelper + @Mock lateinit var site: SiteModel @@ -60,6 +65,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { getButtonUiStateUseCase, mock(), getCategoriesUseCase, + featuredImageHelper, testDispatcher() ) whenever( @@ -76,6 +82,9 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { whenever(postSettingsUtils.getPublishDateLabel(any())).thenReturn(("")) whenever(site.name).thenReturn("") whenever(getCategoriesUseCase.getPostCategoriesString(any(), any())).thenReturn("") + whenever(editPostRepository.getPost()).thenReturn(mock()) + whenever(featuredImageHelper.createCurrentFeaturedImageState(any(), any())) + .thenReturn(FeaturedImageData(FeaturedImageState.IMAGE_EMPTY, null)) // need to observe forever to be able to access `value` since it's a MediatorLiveData viewModel.uiState.observeForever(mock()) @@ -182,6 +191,18 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { assertThat(getHomeUiState(PrepublishingScreenNavigation.FeaturedImage)).isNull() } + @Test + fun `verify featured image action is not propagated when the host cannot edit featured images`() { + // arrange + whenever(editPostRepository.isPage).thenReturn(false) + + // act + viewModel.start(editPostRepository, site, isFeaturedImageEditingSupported = false) + + // assert + assertThat(getHomeUiState(PrepublishingScreenNavigation.FeaturedImage)).isNull() + } + @Test fun `verify that header ui state is propagated to prepublishingHomeUiState once the viewModel is started`() { // arrange diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt index f0752af4e023..4f17694cf55b 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt @@ -96,6 +96,7 @@ class PrepublishingFeaturedImageViewModelTest : BaseUnitTest() { viewModel.onSetOrChangeClicked() assertThat(event?.peekContent()).isEqualTo(POST_ID) + verify(featuredImageHelper).cancelFeaturedImageUpload(site, post, false) verify(featuredImageHelper).trackFeaturedImageEvent(TrackableEvent.IMAGE_SET_CLICKED, POST_ID) } From a567f8328401380a02fa7eda15fc6a2f87e1671d Mon Sep 17 00:00:00 2001 From: adalpari Date: Wed, 2 Sep 2026 17:29:09 +0200 Subject: [PATCH 05/14] CMM-2376: Avoid main-thread media-store read for the home featured row setupHomeUiState runs on the main thread, so use a lightweight FeaturedImageHelper.hasFeaturedImageOrPendingUpload check instead of createCurrentFeaturedImageState, which does a media-store lookup and Photon URL work just to produce a boolean. Co-Authored-By: Claude Opus 4.8 --- .../wordpress/android/ui/posts/FeaturedImageHelper.kt | 10 ++++++++++ .../prepublishing/home/PrepublishingHomeViewModel.kt | 11 +++++------ .../ui/posts/PrepublishingHomeViewModelTest.kt | 5 +---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/FeaturedImageHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/FeaturedImageHelper.kt index 8ee4fbf966ef..8d690e9b272d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/FeaturedImageHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/FeaturedImageHelper.kt @@ -134,6 +134,16 @@ class FeaturedImageHelper @Inject constructor( } } + /** + * Cheap check for whether the post has a featured image set or one being uploaded. Unlike + * [createCurrentFeaturedImageState] it avoids the media-store lookup and Photon URL work, so it + * is safe to call on the main thread when only a boolean is needed. + */ + fun hasFeaturedImageOrPendingUpload(post: PostImmutableModel): Boolean = + post.hasFeaturedImage() || + getFailedFeaturedImageUpload(post) != null || + uploadServiceFacade.getPendingOrInProgressFeaturedImageUploadForPost(post) != null + fun createCurrentFeaturedImageState(site: SiteModel, post: PostImmutableModel): FeaturedImageData { var uploadModel: MediaModel? = uploadServiceFacade.getPendingOrInProgressFeaturedImageUploadForPost(post) if (uploadModel != null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt index 67dedf548700..bca67f89a545 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt @@ -12,7 +12,6 @@ import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.posts.EditPostRepository import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel import org.wordpress.android.ui.posts.FeaturedImageHelper -import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState import org.wordpress.android.ui.posts.GetCategoriesUseCase import org.wordpress.android.ui.posts.GetPostTagsUseCase import org.wordpress.android.ui.posts.PostSettingsUtils @@ -135,7 +134,7 @@ class PrepublishingHomeViewModel @Inject constructor( if (!editPostRepository.isPage && isFeaturedImageEditingSupported) { add(HomeUiState( navigationAction = PrepublishingScreenNavigation.FeaturedImage, - actionResult = if (hasFeaturedImage(editPostRepository, site)) { + actionResult = if (hasFeaturedImage(editPostRepository)) { UiStringRes(R.string.prepublishing_nudges_home_featured_image_set) } else { UiStringRes(R.string.prepublishing_nudges_home_featured_image_not_set) @@ -235,11 +234,11 @@ class PrepublishingHomeViewModel @Inject constructor( } // Reflects an in-progress upload as "set" too, rather than checking the raw id, so a device - // image that is still uploading doesn't briefly read as "Not set". - private fun hasFeaturedImage(editPostRepository: EditPostRepository, site: SiteModel): Boolean { + // image that is still uploading doesn't briefly read as "Not set". Uses the cheap check because + // setupHomeUiState runs on the main thread. + private fun hasFeaturedImage(editPostRepository: EditPostRepository): Boolean { val post = editPostRepository.getPost() ?: return false - return featuredImageHelper.createCurrentFeaturedImageState(site, post).uiState != - FeaturedImageState.IMAGE_EMPTY + return featuredImageHelper.hasFeaturedImageOrPendingUpload(post) } private fun onActionClicked(actionType: ActionType) { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt index 0c612c609ba1..db75886eb407 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt @@ -14,8 +14,6 @@ import org.wordpress.android.R import org.wordpress.android.fluxc.model.PostModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.post.PostStatus -import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData -import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState import org.wordpress.android.fluxc.model.post.PostStatus.PRIVATE import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType.PrepublishingScreenNavigation @@ -83,8 +81,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { whenever(site.name).thenReturn("") whenever(getCategoriesUseCase.getPostCategoriesString(any(), any())).thenReturn("") whenever(editPostRepository.getPost()).thenReturn(mock()) - whenever(featuredImageHelper.createCurrentFeaturedImageState(any(), any())) - .thenReturn(FeaturedImageData(FeaturedImageState.IMAGE_EMPTY, null)) + whenever(featuredImageHelper.hasFeaturedImageOrPendingUpload(any())).thenReturn(false) // need to observe forever to be able to access `value` since it's a MediatorLiveData viewModel.uiState.observeForever(mock()) From 7a80c66349641829a4b549a4d707b7655c147e93 Mon Sep 17 00:00:00 2001 From: adalpari Date: Wed, 2 Sep 2026 18:03:32 +0200 Subject: [PATCH 06/14] CMM-2376: Inline featured image card in the pre-publish sheet Replace the featured-image sub-screen with an inline "Featured image" card on the pre-publish home sheet, matching iOS: a placeholder that opens the picker, and once set, tapping it offers Replace/Remove (and Retry on a failed upload). The card updates live via a merged UI state refreshed off the main thread on post/upload changes. Also pin the publish/submit button as a footer so it stays visible while the sheet scrolls, and compare the card by its rendered content in the diff callback so repeated refreshes don't flicker the image. Removes the now-unused PrepublishingFeaturedImageFragment/ViewModel, its screen navigation, DI wiring and strings. Co-Authored-By: Claude Opus 4.8 --- .../PrepublishingFeaturedImageFragment.kt | 211 ------------------ .../PrepublishingFeaturedImageViewModel.kt | 106 --------- .../prepublishing_featured_image_fragment.xml | 134 ----------- ...prepublishing_home_featured_image_item.xml | 120 ++++++++++ ...PrepublishingFeaturedImageViewModelTest.kt | 150 ------------- 5 files changed, 120 insertions(+), 601 deletions(-) delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt delete mode 100644 WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt delete mode 100644 WordPress/src/main/res/layout/prepublishing_featured_image_fragment.xml create mode 100644 WordPress/src/main/res/layout/prepublishing_home_featured_image_item.xml delete mode 100644 WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt deleted file mode 100644 index e7c70594c388..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageFragment.kt +++ /dev/null @@ -1,211 +0,0 @@ -package org.wordpress.android.ui.posts.prepublishing.featuredimage - -import android.content.Context -import android.os.Bundle -import android.view.View -import android.widget.ImageView.ScaleType -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode.MAIN -import org.wordpress.android.R -import org.wordpress.android.WordPress -import org.wordpress.android.databinding.PrepublishingFeaturedImageFragmentBinding -import org.wordpress.android.databinding.PrepublishingToolbarBinding -import org.wordpress.android.fluxc.Dispatcher -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.store.MediaStore.OnMediaUploaded -import org.wordpress.android.ui.photopicker.MediaPickerLauncher -import org.wordpress.android.ui.posts.EditPostRepository -import org.wordpress.android.ui.posts.EditPostSettingsFragment.EditorDataProvider -import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState -import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel.UiState -import org.wordpress.android.ui.posts.prepublishing.listeners.PrepublishingScreenClosedListener -import org.wordpress.android.ui.utils.UiHelpers -import org.wordpress.android.util.extensions.getSerializableCompat -import org.wordpress.android.util.image.ImageManager -import org.wordpress.android.util.image.ImageType -import org.wordpress.android.viewmodel.observeEvent -import javax.inject.Inject - -class PrepublishingFeaturedImageFragment : Fragment(R.layout.prepublishing_featured_image_fragment) { - private var closeListener: PrepublishingScreenClosedListener? = null - - @Inject - lateinit var viewModelFactory: ViewModelProvider.Factory - - @Inject - lateinit var uiHelpers: UiHelpers - - @Inject - lateinit var imageManager: ImageManager - - @Inject - lateinit var mediaPickerLauncher: MediaPickerLauncher - - @Inject - lateinit var dispatcher: Dispatcher - - private lateinit var viewModel: PrepublishingFeaturedImageViewModel - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - (requireActivity().application as WordPress).component().inject(this) - } - - override fun onStart() { - super.onStart() - dispatcher.register(this) - } - - override fun onStop() { - dispatcher.unregister(this) - super.onStop() - } - - override fun onAttach(context: Context) { - super.onAttach(context) - closeListener = parentFragment as PrepublishingScreenClosedListener - } - - override fun onDetach() { - super.onDetach() - closeListener = null - } - - override fun onResume() { - super.onResume() - // The media picker returns its result to the host activity, so refresh here to reflect - // any image that was just set or queued for upload while this screen was in the background. - if (::viewModel.isInitialized) { - viewModel.refreshFeaturedImageState() - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - with(PrepublishingFeaturedImageFragmentBinding.bind(view)) { - includePrepublishingToolbar.initToolbar() - initClickListeners() - initViewModel() - } - } - - private fun PrepublishingToolbarBinding.initToolbar() { - toolbarTitle.text = getString(R.string.prepublishing_nudges_toolbar_title_featured_image) - backButton.setOnClickListener { viewModel.onBackButtonClick() } - } - - private fun PrepublishingFeaturedImageFragmentBinding.initClickListeners() { - buttonSetFeaturedImage.setOnClickListener { viewModel.onSetOrChangeClicked() } - buttonRemoveFeaturedImage.setOnClickListener { viewModel.onRemoveClicked() } - featuredImageRetryOverlay.setOnClickListener { viewModel.onRetryClicked() } - } - - private fun PrepublishingFeaturedImageFragmentBinding.initViewModel() { - viewModel = ViewModelProvider( - this@PrepublishingFeaturedImageFragment, - viewModelFactory - )[PrepublishingFeaturedImageViewModel::class.java] - - viewModel.uiState.observe(viewLifecycleOwner) { uiState -> - renderUiState(uiState) - } - - viewModel.navigateToHomeScreen.observeEvent(viewLifecycleOwner) { - closeListener?.onBackClicked() - } - - viewModel.launchFeaturedImagePicker.observeEvent(viewLifecycleOwner) { postId -> - mediaPickerLauncher.showFeaturedImagePicker(requireActivity(), getSite(), postId) - } - - viewModel.syncFeaturedImageToEditor.observeEvent(viewLifecycleOwner) { - getEditorDataProvider().syncFeaturedImageIdToEditor() - } - - // updateAsync mutates the shared post and fires postChanged, so this catches an image set - // from the media library, an upload that just completed, and a removal. Observe it raw - // (not observeEvent): postChanged is a single-consumption Event with other observers, so - // observeEvent would miss it whenever another observer handles it first. - getEditPostRepository().postChanged.observe(viewLifecycleOwner) { - viewModel.refreshFeaturedImageState() - } - - viewModel.start(getEditPostRepository(), getSite()) - } - - // TODO CMM-2376 follow-up: this featured-image rendering (state->visibility, overlays, remote vs - // local image) mirrors EditPostSettingsFragment.updateFeaturedImageView. Extract a shared - // component so the two don't drift. - private fun PrepublishingFeaturedImageFragmentBinding.renderUiState(uiState: UiState) { - val state = uiState.featuredImageData.uiState - // A remote image (already on the server) uses postFeaturedImage; a local image being - // uploaded uses postFeaturedImageLocal. Only ever show one so an empty view can't cover the - // other in the FrameLayout. - val isRemote = state == FeaturedImageState.REMOTE_IMAGE_LOADING || - state == FeaturedImageState.REMOTE_IMAGE_SET - val isLocalUpload = state == FeaturedImageState.IMAGE_UPLOAD_IN_PROGRESS || - state == FeaturedImageState.IMAGE_UPLOAD_FAILED - val hasImage = state != FeaturedImageState.IMAGE_EMPTY - with(uiHelpers) { - updateVisibility(featuredImageEmptyText, !hasImage) - updateVisibility(postFeaturedImageContainer, hasImage) - updateVisibility(postFeaturedImage, isRemote) - updateVisibility(postFeaturedImageLocal, isLocalUpload) - updateVisibility(featuredImageRetryOverlay, state.retryOverlayVisible) - updateVisibility(featuredImageProgressOverlay, state.progressOverlayVisible) - updateVisibility(buttonRemoveFeaturedImage, uiState.isRemoveButtonVisible) - } - buttonSetFeaturedImage.text = uiHelpers.getTextOfUiString(requireContext(), uiState.setButtonText) - - val mediaUri = uiState.featuredImageData.mediaUri - when { - isRemote && !mediaUri.isNullOrEmpty() -> { - imageManager.cancelRequestAndClearImageView(postFeaturedImageLocal) - imageManager.load(postFeaturedImage, ImageType.IMAGE, mediaUri, ScaleType.FIT_CENTER) - } - isLocalUpload && !mediaUri.isNullOrEmpty() -> - imageManager.load(postFeaturedImageLocal, ImageType.IMAGE, mediaUri, ScaleType.FIT_CENTER) - else -> { - imageManager.cancelRequestAndClearImageView(postFeaturedImage) - imageManager.cancelRequestAndClearImageView(postFeaturedImageLocal) - } - } - } - - private fun getSite(): SiteModel = - requireNotNull(arguments?.getSerializableCompat(WordPress.SITE)) - - private fun getEditPostRepository(): EditPostRepository = getEditorDataProvider().editPostRepository - - private fun getEditorDataProvider(): EditorDataProvider { - val activity = activity - return if (activity is EditorDataProvider) { - activity - } else { - error("$activity must implement EditorDataProvider") - } - } - - @Suppress("unused") - @Subscribe(threadMode = MAIN) - fun onMediaUploaded(event: OnMediaUploaded) { - val media = event.media - if (media != null && media.markedLocallyAsFeatured) { - viewModel.refreshFeaturedImageState() - } - } - - companion object { - const val TAG = "prepublishing_featured_image_fragment_tag" - - @JvmStatic - fun newInstance(site: SiteModel): PrepublishingFeaturedImageFragment { - val bundle = Bundle().apply { - putSerializable(WordPress.SITE, site) - } - return PrepublishingFeaturedImageFragment().apply { arguments = bundle } - } - } -} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt deleted file mode 100644 index 06d9381262c0..000000000000 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/featuredimage/PrepublishingFeaturedImageViewModel.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.wordpress.android.ui.posts.prepublishing.featuredimage - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import kotlinx.coroutines.CoroutineDispatcher -import org.wordpress.android.R -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.modules.BG_THREAD -import org.wordpress.android.ui.posts.EditPostRepository -import org.wordpress.android.ui.posts.FeaturedImageHelper -import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData -import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState -import org.wordpress.android.ui.posts.FeaturedImageHelper.TrackableEvent -import org.wordpress.android.ui.posts.UpdateFeaturedImageUseCase -import org.wordpress.android.ui.utils.UiString.UiStringRes -import org.wordpress.android.viewmodel.Event -import org.wordpress.android.viewmodel.ScopedViewModel -import javax.inject.Inject -import javax.inject.Named - -class PrepublishingFeaturedImageViewModel @Inject constructor( - private val featuredImageHelper: FeaturedImageHelper, - private val updateFeaturedImageUseCase: UpdateFeaturedImageUseCase, - @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher -) : ScopedViewModel(bgDispatcher) { - private var isStarted = false - private lateinit var editPostRepository: EditPostRepository - private lateinit var siteModel: SiteModel - - private val _uiState = MutableLiveData() - val uiState: LiveData = _uiState - - private val _navigateToHomeScreen = MutableLiveData>() - val navigateToHomeScreen: LiveData> = _navigateToHomeScreen - - private val _launchFeaturedImagePicker = MutableLiveData>() - val launchFeaturedImagePicker: LiveData> = _launchFeaturedImagePicker - - private val _syncFeaturedImageToEditor = MutableLiveData>() - val syncFeaturedImageToEditor: LiveData> = _syncFeaturedImageToEditor - - fun start(editPostRepository: EditPostRepository, siteModel: SiteModel) { - this.editPostRepository = editPostRepository - this.siteModel = siteModel - if (isStarted) return - isStarted = true - refreshFeaturedImageState() - } - - fun onSetOrChangeClicked() { - val post = editPostRepository.getPost() ?: return - // Replacing an image mid-upload would otherwise leave two uploads marked as featured. - featuredImageHelper.cancelFeaturedImageUpload(siteModel, post, false) - featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_SET_CLICKED, post.id) - _launchFeaturedImagePicker.postValue(Event(post.id)) - } - - fun onRemoveClicked() { - val post = editPostRepository.getPost() ?: return - featuredImageHelper.cancelFeaturedImageUpload(siteModel, post, false) - featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_REMOVE_CLICKED, post.id) - updateFeaturedImageUseCase.updateFeaturedImage(NO_FEATURED_IMAGE_ID, editPostRepository) { - refreshFeaturedImageState() - // The model is the source of truth, but the editor caches the featured image separately, - // so tell the host to push the now-cleared id to the editor. - _syncFeaturedImageToEditor.postValue(Event(Unit)) - } - } - - fun onRetryClicked() { - val post = editPostRepository.getPost() ?: return - featuredImageHelper.retryFeaturedImageUpload(siteModel, post) - refreshFeaturedImageState() - } - - fun onBackButtonClick() { - _navigateToHomeScreen.postValue(Event(Unit)) - } - - fun refreshFeaturedImageState() { - val post = editPostRepository.getPost() ?: return - val featuredImageData = featuredImageHelper.createCurrentFeaturedImageState(siteModel, post) - val hasImage = featuredImageData.uiState != FeaturedImageState.IMAGE_EMPTY - _uiState.postValue( - UiState( - featuredImageData = featuredImageData, - setButtonText = if (hasImage) { - UiStringRes(R.string.post_settings_choose_featured_image) - } else { - UiStringRes(R.string.post_settings_set_featured_image) - }, - isRemoveButtonVisible = hasImage - ) - ) - } - - data class UiState( - val featuredImageData: FeaturedImageData, - val setButtonText: UiStringRes, - val isRemoveButtonVisible: Boolean - ) - - companion object { - private const val NO_FEATURED_IMAGE_ID = 0L - } -} diff --git a/WordPress/src/main/res/layout/prepublishing_featured_image_fragment.xml b/WordPress/src/main/res/layout/prepublishing_featured_image_fragment.xml deleted file mode 100644 index 57c431e66df8..000000000000 --- a/WordPress/src/main/res/layout/prepublishing_featured_image_fragment.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/src/main/res/layout/prepublishing_home_featured_image_item.xml b/WordPress/src/main/res/layout/prepublishing_home_featured_image_item.xml new file mode 100644 index 000000000000..b0adc6992059 --- /dev/null +++ b/WordPress/src/main/res/layout/prepublishing_home_featured_image_item.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt deleted file mode 100644 index 4f17694cf55b..000000000000 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingFeaturedImageViewModelTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -package org.wordpress.android.ui.posts.prepublishing - -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.any -import org.mockito.kotlin.eq -import org.mockito.kotlin.never -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import org.wordpress.android.BaseUnitTest -import org.wordpress.android.R -import org.wordpress.android.fluxc.model.PostImmutableModel -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.ui.posts.EditPostRepository -import org.wordpress.android.ui.posts.FeaturedImageHelper -import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData -import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState -import org.wordpress.android.ui.posts.FeaturedImageHelper.TrackableEvent -import org.wordpress.android.ui.posts.UpdateFeaturedImageUseCase -import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel -import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel.UiState -import org.wordpress.android.ui.utils.UiString.UiStringRes -import org.wordpress.android.viewmodel.Event - -private const val POST_ID = 1 - -@ExperimentalCoroutinesApi -class PrepublishingFeaturedImageViewModelTest : BaseUnitTest() { - private lateinit var viewModel: PrepublishingFeaturedImageViewModel - - @Mock - lateinit var featuredImageHelper: FeaturedImageHelper - - @Mock - lateinit var updateFeaturedImageUseCase: UpdateFeaturedImageUseCase - - @Mock - lateinit var editPostRepository: EditPostRepository - - @Mock - lateinit var post: PostImmutableModel - - private val site: SiteModel = SiteModel() - - @Before - fun setup() { - viewModel = PrepublishingFeaturedImageViewModel( - featuredImageHelper, - updateFeaturedImageUseCase, - testDispatcher() - ) - whenever(editPostRepository.getPost()).thenReturn(post) - whenever(post.id).thenReturn(POST_ID) - stubFeaturedImageState(FeaturedImageState.IMAGE_EMPTY) - } - - private fun stubFeaturedImageState(state: FeaturedImageState) { - whenever(featuredImageHelper.createCurrentFeaturedImageState(site, post)) - .thenReturn(FeaturedImageData(state, null)) - } - - @Test - fun `when started with no featured image the remove button is hidden and set button shows set text`() { - var uiState: UiState? = null - viewModel.uiState.observeForever { uiState = it } - - viewModel.start(editPostRepository, site) - - assertThat(uiState?.isRemoveButtonVisible).isFalse - assertThat((uiState?.setButtonText as UiStringRes).stringRes) - .isEqualTo(R.string.post_settings_set_featured_image) - } - - @Test - fun `when started with a featured image the remove button is shown and set button shows change text`() { - stubFeaturedImageState(FeaturedImageState.REMOTE_IMAGE_LOADING) - var uiState: UiState? = null - viewModel.uiState.observeForever { uiState = it } - - viewModel.start(editPostRepository, site) - - assertThat(uiState?.isRemoveButtonVisible).isTrue - assertThat((uiState?.setButtonText as UiStringRes).stringRes) - .isEqualTo(R.string.post_settings_choose_featured_image) - } - - @Test - fun `when set or change is clicked the featured image picker is launched with the post id`() { - var event: Event? = null - viewModel.launchFeaturedImagePicker.observeForever { event = it } - - viewModel.start(editPostRepository, site) - viewModel.onSetOrChangeClicked() - - assertThat(event?.peekContent()).isEqualTo(POST_ID) - verify(featuredImageHelper).cancelFeaturedImageUpload(site, post, false) - verify(featuredImageHelper).trackFeaturedImageEvent(TrackableEvent.IMAGE_SET_CLICKED, POST_ID) - } - - @Test - fun `when remove is clicked the featured image is cleared and pending upload is cancelled`() { - viewModel.start(editPostRepository, site) - viewModel.onRemoveClicked() - - verify(featuredImageHelper).cancelFeaturedImageUpload(site, post, false) - verify(updateFeaturedImageUseCase).updateFeaturedImage(eq(0L), eq(editPostRepository), any()) - } - - @Test - fun `when remove completes the editor is asked to sync`() { - whenever(updateFeaturedImageUseCase.updateFeaturedImage(eq(0L), eq(editPostRepository), any())) - .thenAnswer { invocation -> - @Suppress("UNCHECKED_CAST") - (invocation.arguments[2] as (PostImmutableModel) -> Unit).invoke(post) - } - var event: Event? = null - viewModel.syncFeaturedImageToEditor.observeForever { event = it } - - viewModel.start(editPostRepository, site) - viewModel.onRemoveClicked() - - assertThat(event).isNotNull - } - - @Test - fun `when set or change is clicked without a post the picker is not launched`() { - var event: Event? = null - viewModel.launchFeaturedImagePicker.observeForever { event = it } - - viewModel.start(editPostRepository, site) - whenever(editPostRepository.getPost()).thenReturn(null) - viewModel.onSetOrChangeClicked() - - assertThat(event).isNull() - verify(featuredImageHelper, never()).trackFeaturedImageEvent(any(), any()) - } - - @Test - fun `when back is clicked navigateToHomeScreen is triggered`() { - var event: Event? = null - viewModel.navigateToHomeScreen.observeForever { event = it } - - viewModel.onBackButtonClick() - - assertThat(event).isNotNull - } -} From 5f96f59f8ab9a78a000383a11c1086df64900cbd Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 3 Sep 2026 09:24:39 +0200 Subject: [PATCH 07/14] CMM-2376: Complete inline featured image card wiring The previous commit only captured the file deletions and the new card layout; the code that renders and drives the card (adapter, view holder, item state, view model, fragment, diff callback, layout and the pinned publish button) was left unstaged. Commit it so the branch builds. Also guard the featured-image view holder against a Hidden item instead of requiring Visible (per review feedback). Co-Authored-By: Claude Opus 4.8 --- .../android/modules/AppComponent.java | 3 - .../android/modules/ViewModelModule.java | 6 - .../android/ui/posts/FeaturedImageHelper.kt | 10 -- .../PrepublishingBottomSheetFragment.kt | 9 -- .../prepublishing/PrepublishingViewModel.kt | 5 +- .../home/PrepublishingHomeAdapter.kt | 5 + .../home/PrepublishingHomeDiffCallback.kt | 19 ++- .../home/PrepublishingHomeFragment.kt | 60 +++++++++ .../home/PrepublishingHomeItemUiState.kt | 18 ++- .../home/PrepublishingHomeViewHolder.kt | 87 +++++++++++++ .../home/PrepublishingHomeViewModel.kt | 121 +++++++++++++----- .../post_prepublishing_home_fragment.xml | 44 +++++-- WordPress/src/main/res/values/strings.xml | 4 - .../posts/PrepublishingHomeViewModelTest.kt | 38 +++--- 14 files changed, 330 insertions(+), 99 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java index 13645079eef6..7c9d265446b3 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/AppComponent.java @@ -99,7 +99,6 @@ import org.wordpress.android.ui.posts.prepublishing.PrepublishingBottomSheetFragment; import org.wordpress.android.ui.posts.prepublishing.categories.PrepublishingCategoriesFragment; import org.wordpress.android.ui.posts.prepublishing.categories.addcategory.PrepublishingAddCategoryFragment; -import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageFragment; import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeAdapter; import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeFragment; import org.wordpress.android.ui.posts.EditorLauncher; @@ -457,8 +456,6 @@ public interface AppComponent { void inject(PrepublishingCategoriesFragment object); - void inject(PrepublishingFeaturedImageFragment object); - void inject(PrepublishingAddCategoryFragment object); void inject(PrepublishingSocialFragment object); diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java index e820a76b4956..b94a64bac61d 100644 --- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java +++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java @@ -40,7 +40,6 @@ import org.wordpress.android.ui.posts.prepublishing.PrepublishingViewModel; import org.wordpress.android.ui.posts.prepublishing.categories.PrepublishingCategoriesViewModel; import org.wordpress.android.ui.posts.prepublishing.categories.addcategory.PrepublishingAddCategoryViewModel; -import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageViewModel; import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewModel; import org.wordpress.android.ui.posts.prepublishing.publishsettings.PrepublishingPublishSettingsViewModel; import org.wordpress.android.ui.posts.prepublishing.tags.PrepublishingTagsViewModel; @@ -368,11 +367,6 @@ abstract class ViewModelModule { @ViewModelKey(PrepublishingCategoriesViewModel.class) abstract ViewModel prepublishingCategoriesViewModel(PrepublishingCategoriesViewModel viewModel); - @Binds - @IntoMap - @ViewModelKey(PrepublishingFeaturedImageViewModel.class) - abstract ViewModel prepublishingFeaturedImageViewModel(PrepublishingFeaturedImageViewModel viewModel); - @Binds @IntoMap @ViewModelKey(PrepublishingAddCategoryViewModel.class) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/FeaturedImageHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/FeaturedImageHelper.kt index 8d690e9b272d..8ee4fbf966ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/FeaturedImageHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/FeaturedImageHelper.kt @@ -134,16 +134,6 @@ class FeaturedImageHelper @Inject constructor( } } - /** - * Cheap check for whether the post has a featured image set or one being uploaded. Unlike - * [createCurrentFeaturedImageState] it avoids the media-store lookup and Photon URL work, so it - * is safe to call on the main thread when only a boolean is needed. - */ - fun hasFeaturedImageOrPendingUpload(post: PostImmutableModel): Boolean = - post.hasFeaturedImage() || - getFailedFeaturedImageUpload(post) != null || - uploadServiceFacade.getPendingOrInProgressFeaturedImageUploadForPost(post) != null - fun createCurrentFeaturedImageState(site: SiteModel, post: PostImmutableModel): FeaturedImageData { var uploadModel: MediaModel? = uploadServiceFacade.getPendingOrInProgressFeaturedImageUploadForPost(post) if (uploadModel != null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetFragment.kt index f5cfa67f878f..7323af942cc6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingBottomSheetFragment.kt @@ -32,14 +32,12 @@ import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel.ActionEvent.O import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel.ActionEvent.OpenSubscribeJetpackSocial import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.ADD_CATEGORY import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.CATEGORIES -import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.FEATURED_IMAGE import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.HOME import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.PUBLISH import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.SOCIAL import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.TAGS import org.wordpress.android.ui.posts.prepublishing.categories.PrepublishingCategoriesFragment import org.wordpress.android.ui.posts.prepublishing.categories.addcategory.PrepublishingAddCategoryFragment -import org.wordpress.android.ui.posts.prepublishing.featuredimage.PrepublishingFeaturedImageFragment import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeFragment import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType import org.wordpress.android.ui.posts.prepublishing.home.PublishPost @@ -243,13 +241,6 @@ class PrepublishingBottomSheetFragment : WPBottomSheetDialogFragment(), PrepublishingSocialFragment.TAG ) } - - FEATURED_IMAGE -> { - Pair( - PrepublishingFeaturedImageFragment.newInstance(navigationTarget.site), - PrepublishingFeaturedImageFragment.TAG - ) - } } fadeInFragment(fragment, tag) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt index c9288af77080..8ef6138a9f61 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/PrepublishingViewModel.kt @@ -15,7 +15,6 @@ import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.TaxonomyStore.OnTaxonomyChanged import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.ADD_CATEGORY import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.CATEGORIES -import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.FEATURED_IMAGE import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.HOME import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen.PUBLISH import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType @@ -102,8 +101,7 @@ class PrepublishingViewModel @Inject constructor(private val dispatcher: Dispatc // For the case where this is not a story and hence there's no EditText in the HOME screen, we're ok too, // because there wouldn't have been a keyboard up anyway. if (prepublishingScreen == PUBLISH || - prepublishingScreen == CATEGORIES || - prepublishingScreen == FEATURED_IMAGE + prepublishingScreen == CATEGORIES ) { _dismissKeyboard.postValue(Event(Unit)) } @@ -200,7 +198,6 @@ enum class PrepublishingScreen : Parcelable { CATEGORIES, ADD_CATEGORY, SOCIAL, - FEATURED_IMAGE, } data class PrepublishingNavigationTarget( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeAdapter.kt index 4640213c87e6..e08ba91253e0 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeAdapter.kt @@ -6,9 +6,11 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.wordpress.android.WordPress import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ButtonUiState +import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.FeaturedImageUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HeaderUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HomeUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.SocialUiState +import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHolder.PrepublishingFeaturedImageViewHolder import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHolder.PrepublishingHeaderListItemViewHolder import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHolder.PrepublishingHomeListItemViewHolder import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHolder.PrepublishingSocialItemViewHolder @@ -21,6 +23,7 @@ private const val VIEW_TYPE_HEADER_VIEW: Int = 1 private const val VIEW_TYPE_HOME_ITEM: Int = 2 private const val VIEW_TYPE_SUBMIT_BUTTON: Int = 3 private const val VIEW_TYPE_SOCIAL_ITEM: Int = 4 +private const val VIEW_TYPE_FEATURED_IMAGE_ITEM: Int = 5 class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter() { private var items: List = listOf() @@ -45,6 +48,7 @@ class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter PrepublishingHomeListItemViewHolder(parent, uiHelpers) VIEW_TYPE_SUBMIT_BUTTON -> PrepublishingSubmitButtonViewHolder(parent, uiHelpers) VIEW_TYPE_SOCIAL_ITEM -> PrepublishingSocialItemViewHolder(parent, uiHelpers) + VIEW_TYPE_FEATURED_IMAGE_ITEM -> PrepublishingFeaturedImageViewHolder(parent, uiHelpers, imageManager) else -> throw NotImplementedError("Unknown ViewType") } } @@ -70,6 +74,7 @@ class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter VIEW_TYPE_HOME_ITEM is ButtonUiState -> VIEW_TYPE_SUBMIT_BUTTON is SocialUiState -> VIEW_TYPE_SOCIAL_ITEM + is FeaturedImageUiState -> VIEW_TYPE_FEATURED_IMAGE_ITEM } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeDiffCallback.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeDiffCallback.kt index 50a4326b83c6..8ca67659f2c4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeDiffCallback.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeDiffCallback.kt @@ -1,6 +1,7 @@ package org.wordpress.android.ui.posts.prepublishing.home import androidx.recyclerview.widget.DiffUtil +import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.FeaturedImageUiState class PrepublishingHomeDiffCallback( private val oldList: List, @@ -10,6 +11,11 @@ class PrepublishingHomeDiffCallback( val newItem = newList[newItemPosition] val oldItem = oldList[oldItemPosition] + // The featured-image card refreshes often (upload progress, post changes) with fresh + // callback instances, so treat it as the same item to rebind in place rather than + // remove+insert, which reloads the image and flickers. + if (oldItem is FeaturedImageUiState && newItem is FeaturedImageUiState) return true + return (oldItem == newItem) } @@ -20,5 +26,16 @@ class PrepublishingHomeDiffCallback( override fun areContentsTheSame( oldItemPosition: Int, newItemPosition: Int - ): Boolean = oldList[oldItemPosition] == newList[newItemPosition] + ): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + + // Compare only the rendered content, ignoring the (always-changing) callback instances, so + // an unchanged featured-image state doesn't trigger an unnecessary rebind. + if (oldItem is FeaturedImageUiState.Visible && newItem is FeaturedImageUiState.Visible) { + return oldItem.featuredImageData == newItem.featuredImageData + } + + return oldItem == newItem + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt index 5ebf60482da0..bdf594505d23 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt @@ -7,10 +7,15 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode.MAIN import org.wordpress.android.R import org.wordpress.android.WordPress import org.wordpress.android.databinding.PostPrepublishingHomeFragmentBinding +import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.MediaStore.OnMediaUploaded +import org.wordpress.android.ui.photopicker.MediaPickerLauncher import org.wordpress.android.ui.posts.EditPostRepository import org.wordpress.android.ui.posts.EditPostSettingsFragment import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel @@ -30,6 +35,12 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag @Inject lateinit var imageManager: ImageManager + @Inject + lateinit var mediaPickerLauncher: MediaPickerLauncher + + @Inject + lateinit var dispatcher: Dispatcher + @Inject internal lateinit var viewModelFactory: ViewModelProvider.Factory private lateinit var viewModel: PrepublishingHomeViewModel @@ -42,6 +53,25 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag (requireNotNull(activity).application as WordPress).component().inject(this) } + override fun onStart() { + super.onStart() + dispatcher.register(this) + } + + override fun onStop() { + dispatcher.unregister(this) + super.onStop() + } + + override fun onResume() { + super.onResume() + // The media picker returns its result to the host activity, so refresh here to reflect an + // image that was set or queued while the sheet was in the background. + if (::viewModel.isInitialized) { + viewModel.refreshFeaturedImage() + } + } + override fun onAttach(context: Context) { super.onAttach(context) actionClickedListener = parentFragment as PrepublishingActionClickedListener @@ -102,6 +132,27 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag actionClickedListener?.onSubmitButtonClicked(publishPost) } + viewModel.buttonUiState.observe(viewLifecycleOwner) { buttonUiState -> + publishButton.text = uiHelpers.getTextOfUiString(requireContext(), buttonUiState.buttonText) + publishButton.setOnClickListener { + buttonUiState.onButtonClicked?.invoke(buttonUiState.publishPost) + } + } + + viewModel.launchFeaturedImagePicker.observeEvent(viewLifecycleOwner) { postId -> + mediaPickerLauncher.showFeaturedImagePicker(requireActivity(), getSite(), postId) + } + + viewModel.syncFeaturedImageToEditor.observeEvent(viewLifecycleOwner) { + getEditorDataProvider()?.syncFeaturedImageIdToEditor() + } + + // updateAsync mutates the shared post and fires postChanged; observe it raw (not observeEvent) + // since it is a single-consumption Event shared with other observers. + getEditPostRepository().postChanged.observe(viewLifecycleOwner) { + viewModel.refreshFeaturedImage() + } + viewModel.start(getEditPostRepository(), getSite(), isFeaturedImageEditingSupported()) } @@ -150,6 +201,15 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag } } + @Suppress("unused") + @Subscribe(threadMode = MAIN) + fun onMediaUploaded(event: OnMediaUploaded) { + val media = event.media + if (media != null && media.markedLocallyAsFeatured && ::viewModel.isInitialized) { + viewModel.refreshFeaturedImage() + } + } + companion object { const val TAG = "prepublishing_home_fragment_tag" diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeItemUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeItemUiState.kt index a22924fa8a64..a55a92b734e5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeItemUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeItemUiState.kt @@ -4,6 +4,7 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import org.wordpress.android.R import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData import org.wordpress.android.ui.posts.prepublishing.PrepublishingScreen import org.wordpress.android.ui.utils.UiString import org.wordpress.android.ui.utils.UiString.UiStringRes @@ -82,6 +83,19 @@ sealed class PrepublishingHomeItemUiState( ) } + sealed class FeaturedImageUiState( + isVisible: Boolean = true + ) : PrepublishingHomeItemUiState(isVisible) { + data class Visible( + val featuredImageData: FeaturedImageData, + val onSetOrReplaceClicked: () -> Unit, + val onRemoveClicked: () -> Unit, + val onRetryClicked: () -> Unit, + ) : FeaturedImageUiState() + + object Hidden : FeaturedImageUiState(isVisible = false) + } + sealed interface ActionType { sealed class PrepublishingScreenNavigation( val textRes: UiStringRes, @@ -99,10 +113,6 @@ sealed class PrepublishingHomeItemUiState( UiStringRes(R.string.prepublishing_nudges_categories_action), PrepublishingScreen.CATEGORIES, ) - object FeaturedImage : PrepublishingScreenNavigation( - UiStringRes(R.string.prepublishing_nudges_featured_image_action), - PrepublishingScreen.FEATURED_IMAGE, - ) object AddCategory : PrepublishingScreenNavigation( UiStringRes(R.string.prepublishing_nudges_categories_action), PrepublishingScreen.ADD_CATEGORY, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt index b50b0d02f325..371b506d4d9f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt @@ -5,6 +5,8 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.ImageView +import android.widget.ImageView.ScaleType +import android.widget.PopupMenu import android.widget.TextView import androidx.annotation.LayoutRes import androidx.compose.foundation.clickable @@ -22,7 +24,9 @@ import androidx.recyclerview.widget.RecyclerView import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppThemeM3 import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel.JetpackSocialUiState +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ButtonUiState +import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.FeaturedImageUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HeaderUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HomeUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.SocialUiState @@ -163,4 +167,87 @@ sealed class PrepublishingHomeViewHolder( } } } + + class PrepublishingFeaturedImageViewHolder( + parentView: ViewGroup, + val uiHelpers: UiHelpers, + val imageManager: ImageManager + ) : PrepublishingHomeViewHolder(parentView, R.layout.prepublishing_home_featured_image_item) { + private val card: View = itemView.findViewById(R.id.featured_image_card) + private val placeholder: View = itemView.findViewById(R.id.featured_image_placeholder) + private val remoteImage: ImageView = itemView.findViewById(R.id.post_featured_image) + private val localImage: ImageView = itemView.findViewById(R.id.post_featured_image_local) + private val retryOverlay: View = itemView.findViewById(R.id.featured_image_retry_overlay) + private val progressOverlay: View = itemView.findViewById(R.id.featured_image_progress_overlay) + + override fun onBind(uiState: PrepublishingHomeItemUiState) { + // Hidden items are filtered out before reaching the adapter, but guard rather than crash + // if one ever does. + val uiState = uiState as? FeaturedImageUiState.Visible ?: return + val state = uiState.featuredImageData.uiState + // A remote image (on the server) uses one ImageView, a local upload another, so an empty + // view can never cover a loaded image in the FrameLayout. + val isRemote = state == FeaturedImageState.REMOTE_IMAGE_LOADING || + state == FeaturedImageState.REMOTE_IMAGE_SET + val isLocalUpload = state == FeaturedImageState.IMAGE_UPLOAD_IN_PROGRESS || + state == FeaturedImageState.IMAGE_UPLOAD_FAILED + val isEmpty = state == FeaturedImageState.IMAGE_EMPTY + + with(uiHelpers) { + updateVisibility(placeholder, isEmpty) + updateVisibility(remoteImage, isRemote) + updateVisibility(localImage, isLocalUpload) + updateVisibility(retryOverlay, state.retryOverlayVisible) + updateVisibility(progressOverlay, state.progressOverlayVisible) + } + + val mediaUri = uiState.featuredImageData.mediaUri + when { + isRemote && !mediaUri.isNullOrEmpty() -> { + imageManager.cancelRequestAndClearImageView(localImage) + imageManager.load(remoteImage, ImageType.IMAGE, mediaUri, ScaleType.CENTER_CROP) + } + isLocalUpload && !mediaUri.isNullOrEmpty() -> + imageManager.load(localImage, ImageType.IMAGE, mediaUri, ScaleType.CENTER_CROP) + else -> { + imageManager.cancelRequestAndClearImageView(remoteImage) + imageManager.cancelRequestAndClearImageView(localImage) + } + } + + card.setOnClickListener { anchor -> + when { + isEmpty -> uiState.onSetOrReplaceClicked() + state == FeaturedImageState.IMAGE_UPLOAD_FAILED -> showOptionsMenu(anchor, uiState, retry = true) + else -> showOptionsMenu(anchor, uiState, retry = false) + } + } + } + + private fun showOptionsMenu(anchor: View, uiState: FeaturedImageUiState.Visible, retry: Boolean) { + PopupMenu(anchor.context, anchor).apply { + if (retry) { + menu.add(0, MENU_RETRY, 0, R.string.post_settings_retry_featured_image) + } else { + menu.add(0, MENU_REPLACE, 0, R.string.post_settings_choose_featured_image) + } + menu.add(0, MENU_REMOVE, 1, R.string.post_settings_remove_featured_image) + setOnMenuItemClickListener { item -> + when (item.itemId) { + MENU_REPLACE -> uiState.onSetOrReplaceClicked() + MENU_RETRY -> uiState.onRetryClicked() + MENU_REMOVE -> uiState.onRemoveClicked() + } + true + } + show() + } + } + + companion object { + private const val MENU_REPLACE = 1 + private const val MENU_REMOVE = 2 + private const val MENU_RETRY = 3 + } + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt index bca67f89a545..4b840f3476a4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt @@ -12,11 +12,15 @@ import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.ui.posts.EditPostRepository import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel import org.wordpress.android.ui.posts.FeaturedImageHelper +import org.wordpress.android.ui.posts.FeaturedImageHelper.TrackableEvent import org.wordpress.android.ui.posts.GetCategoriesUseCase import org.wordpress.android.ui.posts.GetPostTagsUseCase import org.wordpress.android.ui.posts.PostSettingsUtils +import org.wordpress.android.ui.posts.UpdateFeaturedImageUseCase import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType.PrepublishingScreenNavigation +import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ButtonUiState +import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.FeaturedImageUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HeaderUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HomeUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.SocialUiState @@ -40,11 +44,13 @@ class PrepublishingHomeViewModel @Inject constructor( private val analyticsTrackerWrapper: AnalyticsTrackerWrapper, private val getCategoriesUseCase: GetCategoriesUseCase, private val featuredImageHelper: FeaturedImageHelper, + private val updateFeaturedImageUseCase: UpdateFeaturedImageUseCase, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher ) : ScopedViewModel(bgDispatcher) { private var isStarted = false private var updateStoryTitleJob: Job? = null private lateinit var editPostRepository: EditPostRepository + private lateinit var site: SiteModel private var isFeaturedImageEditingSupported = false private val _onActionClicked = MutableLiveData>() @@ -53,18 +59,32 @@ class PrepublishingHomeViewModel @Inject constructor( private val _onSubmitButtonClicked = MutableLiveData>() val onSubmitButtonClicked: LiveData> = _onSubmitButtonClicked + // The publish/submit button is pinned as a footer outside the scrolling list, so it's exposed + // separately rather than as a list item. + private val _buttonUiState = MutableLiveData() + val buttonUiState: LiveData = _buttonUiState + + private val _launchFeaturedImagePicker = MutableLiveData>() + val launchFeaturedImagePicker: LiveData> = _launchFeaturedImagePicker + + private val _syncFeaturedImageToEditor = MutableLiveData>() + val syncFeaturedImageToEditor: LiveData> = _syncFeaturedImageToEditor + private val _uiState = MutableLiveData>() private var _socialUiState: MutableLiveData = MutableLiveData(SocialUiState.Hidden) + private val _featuredImageUiState: MutableLiveData = + MutableLiveData(FeaturedImageUiState.Hidden) val uiState: LiveData> = merge( _uiState, - _socialUiState - ) { list, socialUiState -> + _socialUiState, + _featuredImageUiState + ) { list, socialUiState, featuredImageUiState -> list?.map { item -> - if (item is SocialUiState) { - socialUiState ?: SocialUiState.Hidden - } else { - item + when (item) { + is SocialUiState -> socialUiState ?: SocialUiState.Hidden + is FeaturedImageUiState -> featuredImageUiState ?: FeaturedImageUiState.Hidden + else -> item } } } @@ -75,6 +95,7 @@ class PrepublishingHomeViewModel @Inject constructor( isFeaturedImageEditingSupported: Boolean = true ) { this.editPostRepository = editPostRepository + this.site = site this.isFeaturedImageEditingSupported = isFeaturedImageEditingSupported if (isStarted) return // PrepublishingViewModel already dismisses the sheet when the host has no post, but that @@ -87,6 +108,7 @@ class PrepublishingHomeViewModel @Inject constructor( isStarted = true setupHomeUiState(editPostRepository, site) + refreshFeaturedImage() } private fun setupHomeUiState( @@ -131,29 +153,21 @@ class PrepublishingHomeViewModel @Inject constructor( )) } - if (!editPostRepository.isPage && isFeaturedImageEditingSupported) { - add(HomeUiState( - navigationAction = PrepublishingScreenNavigation.FeaturedImage, - actionResult = if (hasFeaturedImage(editPostRepository)) { - UiStringRes(R.string.prepublishing_nudges_home_featured_image_set) - } else { - UiStringRes(R.string.prepublishing_nudges_home_featured_image_not_set) - }, - actionClickable = true, - onNavigationActionClicked = ::onActionClicked - )) - } + // Placeholder replaced via merge with the live featured-image state (see refreshFeaturedImage). + // Its visibility is driven by that state, so it stays Hidden for pages / unsupported hosts. + add(FeaturedImageUiState.Hidden) add(SocialUiState.Hidden) + }.toList() - add(getButtonUiStateUseCase.getUiState(editPostRepository, site) { publishPost -> + _uiState.postValue(prepublishingHomeUiStateList) + _buttonUiState.postValue( + getButtonUiStateUseCase.getUiState(editPostRepository, site) { publishPost -> launch(bgDispatcher) { waitForStoryTitleJobAndSubmit(publishPost) } - }) - }.toList() - - _uiState.postValue(prepublishingHomeUiStateList) + } + ) } private fun MutableList.showNotSetPost( @@ -233,12 +247,57 @@ class PrepublishingHomeViewModel @Inject constructor( updateStoryTitleJob?.cancel() } - // Reflects an in-progress upload as "set" too, rather than checking the raw id, so a device - // image that is still uploading doesn't briefly read as "Not set". Uses the cheap check because - // setupHomeUiState runs on the main thread. - private fun hasFeaturedImage(editPostRepository: EditPostRepository): Boolean { - val post = editPostRepository.getPost() ?: return false - return featuredImageHelper.hasFeaturedImageOrPendingUpload(post) + /** + * Recomputes the featured-image card off the main thread (createCurrentFeaturedImageState does a + * media-store lookup). Called after any featured-image change and on upload events so the card + * reflects empty -> uploading -> set without leaving the sheet. + */ + fun refreshFeaturedImage() { + if (!isStarted) return + if (editPostRepository.isPage || !isFeaturedImageEditingSupported) { + _featuredImageUiState.postValue(FeaturedImageUiState.Hidden) + return + } + launch(bgDispatcher) { + val post = editPostRepository.getPost() + if (post == null) { + _featuredImageUiState.postValue(FeaturedImageUiState.Hidden) + return@launch + } + val featuredImageData = featuredImageHelper.createCurrentFeaturedImageState(site, post) + _featuredImageUiState.postValue( + FeaturedImageUiState.Visible( + featuredImageData = featuredImageData, + onSetOrReplaceClicked = ::onFeaturedImageSetOrReplaceClicked, + onRemoveClicked = ::onFeaturedImageRemoveClicked, + onRetryClicked = ::onFeaturedImageRetryClicked + ) + ) + } + } + + private fun onFeaturedImageSetOrReplaceClicked() { + val post = editPostRepository.getPost() ?: return + // Replacing mid-upload would otherwise leave two uploads marked as featured. + featuredImageHelper.cancelFeaturedImageUpload(site, post, false) + featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_SET_CLICKED, post.id) + _launchFeaturedImagePicker.postValue(Event(post.id)) + } + + private fun onFeaturedImageRemoveClicked() { + val post = editPostRepository.getPost() ?: return + featuredImageHelper.cancelFeaturedImageUpload(site, post, false) + featuredImageHelper.trackFeaturedImageEvent(TrackableEvent.IMAGE_REMOVE_CLICKED, post.id) + updateFeaturedImageUseCase.updateFeaturedImage(NO_FEATURED_IMAGE_ID, editPostRepository) { + refreshFeaturedImage() + _syncFeaturedImageToEditor.postValue(Event(Unit)) + } + } + + private fun onFeaturedImageRetryClicked() { + val post = editPostRepository.getPost() ?: return + featuredImageHelper.retryFeaturedImageUpload(site, post) + refreshFeaturedImage() } private fun onActionClicked(actionType: ActionType) { @@ -258,4 +317,8 @@ class PrepublishingHomeViewModel @Inject constructor( _socialUiState.postValue(newState) } + + companion object { + private const val NO_FEATURED_IMAGE_ID = 0L + } } diff --git a/WordPress/src/main/res/layout/post_prepublishing_home_fragment.xml b/WordPress/src/main/res/layout/post_prepublishing_home_fragment.xml index ee69abe8761a..985cef6f138d 100644 --- a/WordPress/src/main/res/layout/post_prepublishing_home_fragment.xml +++ b/WordPress/src/main/res/layout/post_prepublishing_home_fragment.xml @@ -1,23 +1,41 @@ - + android:layout_height="wrap_content"> - + - - + android:scrollbars="vertical" /> + + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 986bde18ef34..e80047d4d379 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -3821,10 +3821,6 @@ Not set Not set Categories - Featured image - Featured image - Set - Not set Add New Category Add Category Social sharing diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt index db75886eb407..d0752f71ba22 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/PrepublishingHomeViewModelTest.kt @@ -14,11 +14,14 @@ import org.wordpress.android.R import org.wordpress.android.fluxc.model.PostModel import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.post.PostStatus +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageData +import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState import org.wordpress.android.fluxc.model.post.PostStatus.PRIVATE import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ActionType.PrepublishingScreenNavigation import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ButtonUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ButtonUiState.PublishButtonUiState +import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.FeaturedImageUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HeaderUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HomeUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.SocialUiState @@ -51,6 +54,9 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { @Mock lateinit var featuredImageHelper: FeaturedImageHelper + @Mock + lateinit var updateFeaturedImageUseCase: UpdateFeaturedImageUseCase + @Mock lateinit var site: SiteModel @@ -64,6 +70,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { mock(), getCategoriesUseCase, featuredImageHelper, + updateFeaturedImageUseCase, testDispatcher() ) whenever( @@ -81,7 +88,8 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { whenever(site.name).thenReturn("") whenever(getCategoriesUseCase.getPostCategoriesString(any(), any())).thenReturn("") whenever(editPostRepository.getPost()).thenReturn(mock()) - whenever(featuredImageHelper.hasFeaturedImageOrPendingUpload(any())).thenReturn(false) + whenever(featuredImageHelper.createCurrentFeaturedImageState(any(), any())) + .thenReturn(FeaturedImageData(FeaturedImageState.IMAGE_EMPTY, null)) // need to observe forever to be able to access `value` since it's a MediatorLiveData viewModel.uiState.observeForever(mock()) @@ -90,7 +98,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { @Test fun `verify that post home actions are propagated to prepublishingHomeUiState once the viewModel is started`() { // arrange - val expectedActionsAmount = 4 + val expectedActionsAmount = 3 // act viewModel.start(editPostRepository, site) @@ -165,7 +173,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { } @Test - fun `verify that featured image action is propagated to prepublishingHomeUiState once post is not a page`() { + fun `verify featured image card is visible for a post on a supporting host`() { // arrange whenever(editPostRepository.isPage).thenReturn(false) @@ -173,11 +181,11 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { viewModel.start(editPostRepository, site) // assert - assertThat(getHomeUiState(PrepublishingScreenNavigation.FeaturedImage)).isNotNull() + assertThat(getFeaturedImageUiState()).isInstanceOf(FeaturedImageUiState.Visible::class.java) } @Test - fun `verify that featured image action is not propagated to prepublishingHomeUiState once post is a page`() { + fun `verify featured image card is hidden for a page`() { // arrange whenever(editPostRepository.isPage).thenReturn(true) @@ -185,11 +193,11 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { viewModel.start(editPostRepository, site) // assert - assertThat(getHomeUiState(PrepublishingScreenNavigation.FeaturedImage)).isNull() + assertThat(getFeaturedImageUiState()).isEqualTo(FeaturedImageUiState.Hidden) } @Test - fun `verify featured image action is not propagated when the host cannot edit featured images`() { + fun `verify featured image card is hidden when the host cannot edit featured images`() { // arrange whenever(editPostRepository.isPage).thenReturn(false) @@ -197,7 +205,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { viewModel.start(editPostRepository, site, isFeaturedImageEditingSupported = false) // assert - assertThat(getHomeUiState(PrepublishingScreenNavigation.FeaturedImage)).isNull() + assertThat(getFeaturedImageUiState()).isEqualTo(FeaturedImageUiState.Hidden) } @Test @@ -215,17 +223,12 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { } @Test - fun `verify that publish button ui state is propagated to uiState once the viewModel is started`() { - // arrange - val expectedActionsAmount = 1 - + fun `verify that publish button ui state is exposed once the viewModel is started`() { // act viewModel.start(editPostRepository, site) // assert - assertThat(viewModel.uiState.value?.filterIsInstance(ButtonUiState::class.java)?.size).isEqualTo( - expectedActionsAmount - ) + assertThat(viewModel.buttonUiState.value).isInstanceOf(PublishButtonUiState::class.java) } @Test @@ -469,7 +472,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { private fun getHeaderUiState() = viewModel.uiState.value?.filterIsInstance(HeaderUiState::class.java)?.first() private fun getButtonUiState(): ButtonUiState? { - return viewModel.uiState.value?.filterIsInstance(ButtonUiState::class.java)?.first() + return viewModel.buttonUiState.value } private fun getHomeUiState(actionType: ActionType): HomeUiState? { @@ -477,4 +480,7 @@ class PrepublishingHomeViewModelTest : BaseUnitTest() { ?.filterIsInstance(HomeUiState::class.java) return actions?.find { it.navigationAction == actionType } } + + private fun getFeaturedImageUiState(): FeaturedImageUiState? = + viewModel.uiState.value?.filterIsInstance(FeaturedImageUiState::class.java)?.firstOrNull() } From db7bd999c3f607c21be0c93809c5fc1666452d31 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 3 Sep 2026 09:30:29 +0200 Subject: [PATCH 08/14] CMM-2376: Remove dead submit-button list item The publish/submit button is now a pinned footer, so its RecyclerView view type, view holder and layout are unreachable. Remove them; the ButtonUiState branch in getItemViewType now errors since it should never appear as a list item. Co-Authored-By: Claude Opus 4.8 --- .../home/PrepublishingHomeAdapter.kt | 7 ++---- .../home/PrepublishingHomeViewHolder.kt | 19 ---------------- ...blishing_home_publish_button_list_item.xml | 22 ------------------- 3 files changed, 2 insertions(+), 46 deletions(-) delete mode 100644 WordPress/src/main/res/layout/prepublishing_home_publish_button_list_item.xml diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeAdapter.kt index e08ba91253e0..e78cb8ce5aa2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeAdapter.kt @@ -14,14 +14,12 @@ import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHo import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHolder.PrepublishingHeaderListItemViewHolder import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHolder.PrepublishingHomeListItemViewHolder import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHolder.PrepublishingSocialItemViewHolder -import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeViewHolder.PrepublishingSubmitButtonViewHolder import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.image.ImageManager import javax.inject.Inject private const val VIEW_TYPE_HEADER_VIEW: Int = 1 private const val VIEW_TYPE_HOME_ITEM: Int = 2 -private const val VIEW_TYPE_SUBMIT_BUTTON: Int = 3 private const val VIEW_TYPE_SOCIAL_ITEM: Int = 4 private const val VIEW_TYPE_FEATURED_IMAGE_ITEM: Int = 5 @@ -46,7 +44,6 @@ class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter PrepublishingHomeListItemViewHolder(parent, uiHelpers) - VIEW_TYPE_SUBMIT_BUTTON -> PrepublishingSubmitButtonViewHolder(parent, uiHelpers) VIEW_TYPE_SOCIAL_ITEM -> PrepublishingSocialItemViewHolder(parent, uiHelpers) VIEW_TYPE_FEATURED_IMAGE_ITEM -> PrepublishingFeaturedImageViewHolder(parent, uiHelpers, imageManager) else -> throw NotImplementedError("Unknown ViewType") @@ -67,12 +64,12 @@ class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter VIEW_TYPE_HEADER_VIEW is HomeUiState -> VIEW_TYPE_HOME_ITEM - is ButtonUiState -> VIEW_TYPE_SUBMIT_BUTTON + // The publish/submit button is rendered as a pinned footer, not a list item. + is ButtonUiState -> error("ButtonUiState is not rendered in the list") is SocialUiState -> VIEW_TYPE_SOCIAL_ITEM is FeaturedImageUiState -> VIEW_TYPE_FEATURED_IMAGE_ITEM } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt index 371b506d4d9f..8f6236fba1c2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt @@ -3,7 +3,6 @@ package org.wordpress.android.ui.posts.prepublishing.home import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Button import android.widget.ImageView import android.widget.ImageView.ScaleType import android.widget.PopupMenu @@ -25,7 +24,6 @@ import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppThemeM3 import org.wordpress.android.ui.posts.EditorJetpackSocialViewModel.JetpackSocialUiState import org.wordpress.android.ui.posts.FeaturedImageHelper.FeaturedImageState -import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.ButtonUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.FeaturedImageUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HeaderUiState import org.wordpress.android.ui.posts.prepublishing.home.PrepublishingHomeItemUiState.HomeUiState @@ -94,23 +92,6 @@ sealed class PrepublishingHomeViewHolder( } } - class PrepublishingSubmitButtonViewHolder(parentView: ViewGroup, val uiHelpers: UiHelpers) : - PrepublishingHomeViewHolder( - parentView, - R.layout.prepublishing_home_publish_button_list_item - ) { - private val button: Button = itemView.findViewById(R.id.publish_button) - - override fun onBind(uiState: PrepublishingHomeItemUiState) { - uiState as ButtonUiState - - button.text = uiHelpers.getTextOfUiString(itemView.context, uiState.buttonText) - button.setOnClickListener { - uiState.onButtonClicked?.invoke(uiState.publishPost) - } - } - } - class PrepublishingSocialItemViewHolder( parentView: ViewGroup, val uiHelpers: UiHelpers, diff --git a/WordPress/src/main/res/layout/prepublishing_home_publish_button_list_item.xml b/WordPress/src/main/res/layout/prepublishing_home_publish_button_list_item.xml deleted file mode 100644 index dcc8c9a1971f..000000000000 --- a/WordPress/src/main/res/layout/prepublishing_home_publish_button_list_item.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - From c61ea2d593db4b3fe651736022b948ec88cba9db Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 3 Sep 2026 09:45:11 +0200 Subject: [PATCH 09/14] CMM-2376: Harden featured-image refresh against races and blank card Make refreshFeaturedImage single-flight: cancel the previous launch and bail via ensureActive() before posting, so a slower in-flight read can't overwrite the latest state and the redundant reads on sheet open collapse. Fall back to the placeholder when a remote/local image state has no URI to load, instead of showing a blank ImageView. Co-Authored-By: Claude Opus 4.8 --- .../home/PrepublishingHomeViewHolder.kt | 22 ++++++++++++------- .../home/PrepublishingHomeViewModel.kt | 13 ++++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt index 8f6236fba1c2..f728824a2393 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewHolder.kt @@ -174,22 +174,28 @@ sealed class PrepublishingHomeViewHolder( state == FeaturedImageState.IMAGE_UPLOAD_FAILED val isEmpty = state == FeaturedImageState.IMAGE_EMPTY + val mediaUri = uiState.featuredImageData.mediaUri + val hasMediaUri = !mediaUri.isNullOrEmpty() + val showRemoteImage = isRemote && hasMediaUri + val showLocalImage = isLocalUpload && hasMediaUri + with(uiHelpers) { - updateVisibility(placeholder, isEmpty) - updateVisibility(remoteImage, isRemote) - updateVisibility(localImage, isLocalUpload) + // Fall back to the placeholder when a remote/local image state has no URI to load, + // otherwise we'd show a blank ImageView with no indication the image is missing. + updateVisibility(placeholder, isEmpty || (!showRemoteImage && !showLocalImage)) + updateVisibility(remoteImage, showRemoteImage) + updateVisibility(localImage, showLocalImage) updateVisibility(retryOverlay, state.retryOverlayVisible) updateVisibility(progressOverlay, state.progressOverlayVisible) } - val mediaUri = uiState.featuredImageData.mediaUri when { - isRemote && !mediaUri.isNullOrEmpty() -> { + showRemoteImage -> { imageManager.cancelRequestAndClearImageView(localImage) - imageManager.load(remoteImage, ImageType.IMAGE, mediaUri, ScaleType.CENTER_CROP) + imageManager.load(remoteImage, ImageType.IMAGE, mediaUri.orEmpty(), ScaleType.CENTER_CROP) } - isLocalUpload && !mediaUri.isNullOrEmpty() -> - imageManager.load(localImage, ImageType.IMAGE, mediaUri, ScaleType.CENTER_CROP) + showLocalImage -> + imageManager.load(localImage, ImageType.IMAGE, mediaUri.orEmpty(), ScaleType.CENTER_CROP) else -> { imageManager.cancelRequestAndClearImageView(remoteImage) imageManager.cancelRequestAndClearImageView(localImage) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt index 4b840f3476a4..65548f3708b6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job +import kotlinx.coroutines.ensureActive import org.wordpress.android.R import org.wordpress.android.analytics.AnalyticsTracker import org.wordpress.android.fluxc.model.SiteModel @@ -49,6 +50,7 @@ class PrepublishingHomeViewModel @Inject constructor( ) : ScopedViewModel(bgDispatcher) { private var isStarted = false private var updateStoryTitleJob: Job? = null + private var refreshFeaturedImageJob: Job? = null private lateinit var editPostRepository: EditPostRepository private lateinit var site: SiteModel private var isFeaturedImageEditingSupported = false @@ -251,20 +253,29 @@ class PrepublishingHomeViewModel @Inject constructor( * Recomputes the featured-image card off the main thread (createCurrentFeaturedImageState does a * media-store lookup). Called after any featured-image change and on upload events so the card * reflects empty -> uploading -> set without leaving the sheet. + * + * This is invoked from several sources that can fire near-simultaneously (start, onResume, the + * postChanged observer, onMediaUploaded). Runs single-flight: cancelling the previous launch keeps + * the latest state from being overwritten by a slower in-flight one and avoids redundant media-store + * reads for the same unchanged state on sheet open. */ fun refreshFeaturedImage() { if (!isStarted) return + refreshFeaturedImageJob?.cancel() if (editPostRepository.isPage || !isFeaturedImageEditingSupported) { _featuredImageUiState.postValue(FeaturedImageUiState.Hidden) return } - launch(bgDispatcher) { + refreshFeaturedImageJob = launch(bgDispatcher) { val post = editPostRepository.getPost() if (post == null) { _featuredImageUiState.postValue(FeaturedImageUiState.Hidden) return@launch } val featuredImageData = featuredImageHelper.createCurrentFeaturedImageState(site, post) + // Bail if a newer refresh superseded this one while the synchronous lookup ran, so a + // slower/stale result never overwrites the latest state. + ensureActive() _featuredImageUiState.postValue( FeaturedImageUiState.Visible( featuredImageData = featuredImageData, From 5c7cc9e2fe4ca1c34f469cb55a7e9bb429c33775 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 3 Sep 2026 14:17:37 +0200 Subject: [PATCH 10/14] CMM-2376: Match featured image label style to the other rows Use the same subtitle text appearance, title case (not all caps), default text color and top spacing as the Publish Date/Tags/Categories rows instead of an iOS-style uppercase section header. Co-Authored-By: Claude Opus 4.8 --- .../layout/prepublishing_home_featured_image_item.xml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/WordPress/src/main/res/layout/prepublishing_home_featured_image_item.xml b/WordPress/src/main/res/layout/prepublishing_home_featured_image_item.xml index b0adc6992059..648af69646e7 100644 --- a/WordPress/src/main/res/layout/prepublishing_home_featured_image_item.xml +++ b/WordPress/src/main/res/layout/prepublishing_home_featured_image_item.xml @@ -7,18 +7,17 @@ android:orientation="vertical" android:paddingStart="@dimen/margin_extra_large" android:paddingEnd="@dimen/margin_extra_large" - android:paddingTop="@dimen/margin_medium" - android:paddingBottom="@dimen/margin_medium"> + android:paddingTop="@dimen/margin_extra_large" + android:paddingBottom="@dimen/margin_extra_large"> + android:textAlignment="viewStart" + android:textAppearance="?attr/textAppearanceSubtitle1" /> Date: Thu, 3 Sep 2026 14:21:07 +0200 Subject: [PATCH 11/14] CMM-2376: Push featured image to the GutenbergKit editor on set GutenbergKitActivity.setFeaturedImageId took the settings-fragment branch (which writes the id but doesn't touch the editor) and its editor-push else was unreachable, so an image set from the pre-publish sheet never reached the editor. Restructure so the id is written (settings fragment if shown, else the use case) and the editor is always pushed with the mediaId. Also collapse the equivalent dead else in EditPostActivity into a nested fallback and fix its misleading comment; its unconditional sendToJSFeaturedImageId already syncs the editor there. Co-Authored-By: Claude Opus 4.8 --- .../android/ui/posts/EditPostActivity.kt | 13 ++++++++----- .../android/ui/posts/GutenbergKitActivity.kt | 15 +++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index f9408772d1c7..e84b441fe32a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -2755,12 +2755,15 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor mediaId, postRepository ) { _: PostImmutableModel? -> } - } else if (editPostSettingsFragment != null) { - editPostSettingsFragment?.updateFeaturedImage(mediaId, imagePicked) } else { - // The post settings screen isn't shown (e.g. the featured image is being set from the - // pre-publish sheet), so write the id straight to the post ourselves. - updateFeaturedImageUseCase.updateFeaturedImage(mediaId, editPostRepository) { } + // The post settings screen may not be showing (e.g. the image was set from the + // pre-publish sheet), so fall back to writing the id ourselves. Either way the editor is + // synced below via sendToJSFeaturedImageId. + if (editPostSettingsFragment != null) { + editPostSettingsFragment?.updateFeaturedImage(mediaId, imagePicked) + } else { + updateFeaturedImageUseCase.updateFeaturedImage(mediaId, editPostRepository) { } + } } if (editorFragment is GutenbergEditorFragment) { (editorFragment as GutenbergEditorFragment).sendToJSFeaturedImageId(mediaId.toInt()) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt index 2a8eb82a1d09..42e77b96b160 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt @@ -2471,14 +2471,17 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene mediaId, postRepository ) { _: PostImmutableModel? -> } - } else if (editPostSettingsFragment != null) { - editPostSettingsFragment?.updateFeaturedImage(mediaId, imagePicked) } else { - // The post settings screen isn't shown (e.g. the featured image is being set from the - // pre-publish sheet), so write the id ourselves and push it to the editor. - updateFeaturedImageUseCase.updateFeaturedImage(mediaId, editPostRepository) { - editorFragment?.setFeaturedImageId(editPostRepository.featuredImageId) + // The post settings screen may not be showing (e.g. the image was set from the + // pre-publish sheet), so fall back to writing the id ourselves. + if (editPostSettingsFragment != null) { + editPostSettingsFragment?.updateFeaturedImage(mediaId, imagePicked) + } else { + updateFeaturedImageUseCase.updateFeaturedImage(mediaId, editPostRepository) { } } + // Push straight to the editor so it doesn't keep showing a stale image. Uses mediaId + // rather than the repository value because the write above is async. + editorFragment?.setFeaturedImageId(mediaId) } } From f792726271c2bf936ae697cc0e90614b2c3e9101 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 3 Sep 2026 15:16:51 +0200 Subject: [PATCH 12/14] CMM-2376: Push featured image to legacy Gutenberg JS on sheet sync syncFeaturedImageIdToEditor() only stored the id on the fragment, leaving the legacy Gutenberg editor's featured-image block showing a stale image after a set/remove from the pre-publish sheet. Push it to the JS editor via sendToJSFeaturedImageId(), matching the picker path. GutenbergKit's GutenbergView has no app->webview featured-image setter, so the equivalent call there only records the id; correct the misleading comments to document that limitation instead of claiming a push. Co-Authored-By: Claude Opus 4.8 --- .../org/wordpress/android/ui/posts/EditPostActivity.kt | 6 +++++- .../wordpress/android/ui/posts/GutenbergKitActivity.kt | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt index e84b441fe32a..8f9c08e31777 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditPostActivity.kt @@ -3890,7 +3890,11 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor override fun getEditPostRepository() = editPostRepository override fun getSite() = siteModel override fun syncFeaturedImageIdToEditor() { - editorFragment?.setFeaturedImageId(editPostRepository.featuredImageId) + val featuredImageId = editPostRepository.featuredImageId + editorFragment?.setFeaturedImageId(featuredImageId) + // setFeaturedImageId only stores the id; push it to the JS editor so the featured image + // block isn't left showing a stale image after a set/remove from the pre-publish sheet. + (editorFragment as? GutenbergEditorFragment)?.sendToJSFeaturedImageId(featuredImageId.toInt()) } override fun supportsFeaturedImageEditing() = true diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt index 42e77b96b160..34a98677b19a 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitActivity.kt @@ -2479,8 +2479,10 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene } else { updateFeaturedImageUseCase.updateFeaturedImage(mediaId, editPostRepository) { } } - // Push straight to the editor so it doesn't keep showing a stale image. Uses mediaId - // rather than the repository value because the write above is async. + // Record the id on the fragment (kept across config changes). GutenbergKit's + // GutenbergView exposes no app->webview featured-image setter, so the already-loaded + // editor can only pick this up on its next load; the PostModel write above is the + // source of truth for saving. editorFragment?.setFeaturedImageId(mediaId) } } @@ -3139,6 +3141,9 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene override fun getEditPostRepository() = editPostRepository override fun getSite() = siteModel override fun syncFeaturedImageIdToEditor() { + // GutenbergKit's GutenbergView has no app->webview featured-image setter, so this only + // records the id on the fragment; the loaded editor reflects it on its next load. The + // PostModel write done by the caller is what actually persists the change. editorFragment?.setFeaturedImageId(editPostRepository.featuredImageId) } override fun supportsFeaturedImageEditing() = true From 13d83d225c9fd9440d458672c512d7e048dd9064 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 3 Sep 2026 17:11:51 +0200 Subject: [PATCH 13/14] CMM-2376: Seed featured-image card synchronously and scope upload refresh to the post - Build the featured-image card state in setupHomeUiState so it is present in the first frame instead of popping in (and reflowing the sheet) after the background refresh. - Drop the redundant refreshFeaturedImage() in start(); the synchronous seed covers it, leaving onResume / postChanged / onMediaUploaded as the update paths. - Share the state builder between the seed and the async refresh. - Only refresh on OnMediaUploaded when the media belongs to this post, matching the EditPostActivity guard. Co-Authored-By: Claude Fable 5.1 --- .../home/PrepublishingHomeFragment.kt | 8 ++- .../home/PrepublishingHomeViewModel.kt | 56 ++++++++++--------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt index bdf594505d23..2174c2540ee1 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt @@ -205,7 +205,13 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag @Subscribe(threadMode = MAIN) fun onMediaUploaded(event: OnMediaUploaded) { val media = event.media - if (media != null && media.markedLocallyAsFeatured && ::viewModel.isInitialized) { + // Only react to featured-image uploads for the post this sheet is editing; a queued upload + // for a different post must not refresh this card (matches EditPostActivity's guard). + if (media != null && + media.markedLocallyAsFeatured && + media.localPostId == getEditPostRepository().id && + ::viewModel.isInitialized + ) { viewModel.refreshFeaturedImage() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt index 65548f3708b6..6ebff3b2a214 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt @@ -110,13 +110,16 @@ class PrepublishingHomeViewModel @Inject constructor( isStarted = true setupHomeUiState(editPostRepository, site) - refreshFeaturedImage() } private fun setupHomeUiState( editPostRepository: EditPostRepository, site: SiteModel ) { + // Built synchronously alongside the other rows so the card is present in the first frame instead + // of popping in (and reflowing the sheet) once a background refresh completes. Later updates go + // through refreshFeaturedImage, which runs off the main thread. + val featuredImageUiState = createFeaturedImageUiState() val prepublishingHomeUiStateList = mutableListOf().apply { add( HeaderUiState( @@ -155,13 +158,14 @@ class PrepublishingHomeViewModel @Inject constructor( )) } - // Placeholder replaced via merge with the live featured-image state (see refreshFeaturedImage). - // Its visibility is driven by that state, so it stays Hidden for pages / unsupported hosts. - add(FeaturedImageUiState.Hidden) + // Replaced via merge with the live featured-image state; it stays Hidden for pages / + // unsupported hosts. + add(featuredImageUiState) add(SocialUiState.Hidden) }.toList() + _featuredImageUiState.postValue(featuredImageUiState) _uiState.postValue(prepublishingHomeUiStateList) _buttonUiState.postValue( getButtonUiStateUseCase.getUiState(editPostRepository, site) { publishPost -> @@ -252,39 +256,37 @@ class PrepublishingHomeViewModel @Inject constructor( /** * Recomputes the featured-image card off the main thread (createCurrentFeaturedImageState does a * media-store lookup). Called after any featured-image change and on upload events so the card - * reflects empty -> uploading -> set without leaving the sheet. + * reflects empty -> uploading -> set without leaving the sheet. The initial state is seeded + * synchronously by setupHomeUiState, so this only handles updates. * - * This is invoked from several sources that can fire near-simultaneously (start, onResume, the - * postChanged observer, onMediaUploaded). Runs single-flight: cancelling the previous launch keeps - * the latest state from being overwritten by a slower in-flight one and avoids redundant media-store - * reads for the same unchanged state on sheet open. + * onResume, the postChanged observer and onMediaUploaded can fire near-simultaneously. Runs + * single-flight: cancelling the previous launch keeps the latest state from being overwritten by a + * slower in-flight one. Skipping the relaunch while one is active would not be safe, since that job + * may already have read the pre-change state. */ fun refreshFeaturedImage() { if (!isStarted) return refreshFeaturedImageJob?.cancel() - if (editPostRepository.isPage || !isFeaturedImageEditingSupported) { - _featuredImageUiState.postValue(FeaturedImageUiState.Hidden) - return - } refreshFeaturedImageJob = launch(bgDispatcher) { - val post = editPostRepository.getPost() - if (post == null) { - _featuredImageUiState.postValue(FeaturedImageUiState.Hidden) - return@launch - } - val featuredImageData = featuredImageHelper.createCurrentFeaturedImageState(site, post) + val featuredImageUiState = createFeaturedImageUiState() // Bail if a newer refresh superseded this one while the synchronous lookup ran, so a // slower/stale result never overwrites the latest state. ensureActive() - _featuredImageUiState.postValue( - FeaturedImageUiState.Visible( - featuredImageData = featuredImageData, - onSetOrReplaceClicked = ::onFeaturedImageSetOrReplaceClicked, - onRemoveClicked = ::onFeaturedImageRemoveClicked, - onRetryClicked = ::onFeaturedImageRetryClicked - ) - ) + _featuredImageUiState.postValue(featuredImageUiState) + } + } + + private fun createFeaturedImageUiState(): FeaturedImageUiState { + if (editPostRepository.isPage || !isFeaturedImageEditingSupported) { + return FeaturedImageUiState.Hidden } + val post = editPostRepository.getPost() ?: return FeaturedImageUiState.Hidden + return FeaturedImageUiState.Visible( + featuredImageData = featuredImageHelper.createCurrentFeaturedImageState(site, post), + onSetOrReplaceClicked = ::onFeaturedImageSetOrReplaceClicked, + onRemoveClicked = ::onFeaturedImageRemoveClicked, + onRetryClicked = ::onFeaturedImageRetryClicked + ) } private fun onFeaturedImageSetOrReplaceClicked() { From 9cd5559202ac6f6fbb4b1228454b3c3273b62816 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 3 Sep 2026 17:26:53 +0200 Subject: [PATCH 14/14] CMM-2376: Resolve Detekt ComplexCondition and ReturnCount findings - onMediaUploaded: peel the null / initialization checks into early returns so the remaining guard is a two-part condition. - createFeaturedImageUiState: fold the guards into a single conditional expression. No behavior change. Co-Authored-By: Claude Fable 5.1 --- .../home/PrepublishingHomeFragment.kt | 9 +++------ .../home/PrepublishingHomeViewModel.kt | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt index 2174c2540ee1..9e118a8b36f4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeFragment.kt @@ -204,14 +204,11 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag @Suppress("unused") @Subscribe(threadMode = MAIN) fun onMediaUploaded(event: OnMediaUploaded) { - val media = event.media + val media = event.media ?: return + if (!::viewModel.isInitialized) return // Only react to featured-image uploads for the post this sheet is editing; a queued upload // for a different post must not refresh this card (matches EditPostActivity's guard). - if (media != null && - media.markedLocallyAsFeatured && - media.localPostId == getEditPostRepository().id && - ::viewModel.isInitialized - ) { + if (media.markedLocallyAsFeatured && media.localPostId == getEditPostRepository().id) { viewModel.refreshFeaturedImage() } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt index 6ebff3b2a214..239e6e241e9d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/prepublishing/home/PrepublishingHomeViewModel.kt @@ -277,16 +277,17 @@ class PrepublishingHomeViewModel @Inject constructor( } private fun createFeaturedImageUiState(): FeaturedImageUiState { - if (editPostRepository.isPage || !isFeaturedImageEditingSupported) { - return FeaturedImageUiState.Hidden + val post = editPostRepository.getPost() + return if (editPostRepository.isPage || !isFeaturedImageEditingSupported || post == null) { + FeaturedImageUiState.Hidden + } else { + FeaturedImageUiState.Visible( + featuredImageData = featuredImageHelper.createCurrentFeaturedImageState(site, post), + onSetOrReplaceClicked = ::onFeaturedImageSetOrReplaceClicked, + onRemoveClicked = ::onFeaturedImageRemoveClicked, + onRetryClicked = ::onFeaturedImageRetryClicked + ) } - val post = editPostRepository.getPost() ?: return FeaturedImageUiState.Hidden - return FeaturedImageUiState.Visible( - featuredImageData = featuredImageHelper.createCurrentFeaturedImageState(site, post), - onSetOrReplaceClicked = ::onFeaturedImageSetOrReplaceClicked, - onRemoveClicked = ::onFeaturedImageRemoveClicked, - onRetryClicked = ::onFeaturedImageRetryClicked - ) } private fun onFeaturedImageSetOrReplaceClicked() {