Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
a3df425
CMM-2376: Add a Featured image option to the pre-publish sheet
adalpari Sep 2, 2026
12ac6c4
CMM-2376: Fix featured image preview not rendering until reopen
adalpari Sep 2, 2026
e7c906c
CMM-2376: Sync editor featured image on removal; review + codecheck f…
adalpari Sep 2, 2026
3c72746
CMM-2376: Wire featured image across pre-publish hosts; review fixes
adalpari Sep 2, 2026
6b503f7
Merge branch 'trunk' into adalpari/add-android-label-support-tickets
adalpari Sep 2, 2026
a567f83
CMM-2376: Avoid main-thread media-store read for the home featured row
adalpari Sep 2, 2026
7a80c66
CMM-2376: Inline featured image card in the pre-publish sheet
adalpari Sep 2, 2026
9ad5656
Merge branch 'trunk' into cmm-2376-featured-image-pre-publish-sheet
adalpari Sep 3, 2026
5f96f59
CMM-2376: Complete inline featured image card wiring
adalpari Sep 3, 2026
db7bd99
CMM-2376: Remove dead submit-button list item
adalpari Sep 3, 2026
c61ea2d
CMM-2376: Harden featured-image refresh against races and blank card
adalpari Sep 3, 2026
5c7cc9e
CMM-2376: Match featured image label style to the other rows
adalpari Sep 3, 2026
401ea91
CMM-2376: Push featured image to the GutenbergKit editor on set
adalpari Sep 3, 2026
f792726
CMM-2376: Push featured image to legacy Gutenberg JS on sheet sync
adalpari Sep 3, 2026
13d83d2
CMM-2376: Seed featured-image card synchronously and scope upload ref…
adalpari Sep 3, 2026
9cd5559
CMM-2376: Resolve Detekt ComplexCondition and ReturnCount findings
adalpari Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2755,8 +2755,15 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor
mediaId, postRepository
) { _: PostImmutableModel? ->
}
} else if (editPostSettingsFragment != null) {
editPostSettingsFragment?.updateFeaturedImage(mediaId, imagePicked)
} else {
// 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())
Expand Down Expand Up @@ -3882,6 +3889,14 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor
// EditorDataProvider methods
override fun getEditPostRepository() = editPostRepository
override fun getSite() = siteModel
override fun syncFeaturedImageIdToEditor() {
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

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ 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();

/**
* 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2471,8 +2471,19 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene
mediaId, postRepository
) { _: PostImmutableModel? ->
}
} else if (editPostSettingsFragment != null) {
editPostSettingsFragment?.updateFeaturedImage(mediaId, imagePicked)
} else {
// 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) { }
}
// 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)
}
}

Expand Down Expand Up @@ -3129,6 +3140,13 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene
// EditorDataProvider methods
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

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ 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.
}
// 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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ 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
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

class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter<PrepublishingHomeViewHolder>() {
private var items: List<PrepublishingHomeItemUiState> = listOf()
Expand All @@ -43,8 +44,8 @@ class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter<Prepubli
imageManager
)
VIEW_TYPE_HOME_ITEM -> 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")
}
}
Expand All @@ -63,13 +64,14 @@ class PrepublishingHomeAdapter(context: Context) : RecyclerView.Adapter<Prepubli
diffResult.dispatchUpdatesTo(this)
}

@Suppress("UseCheckOrError")
override fun getItemViewType(position: Int): Int {
return when (items[position]) {
is HeaderUiState -> 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
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<PrepublishingHomeItemUiState>,
Expand All @@ -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)
}

Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -102,9 +132,33 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag
actionClickedListener?.onSubmitButtonClicked(publishPost)
}

viewModel.start(getEditPostRepository(), getSite())
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())
}

private fun isFeaturedImageEditingSupported(): Boolean =
getEditorDataProvider()?.supportsFeaturedImageEditing() ?: false

private fun setupJetpackSocialViewModel() {
jetpackSocialViewModel = (parentFragment as PrepublishingSocialViewModelProvider)
.getEditorJetpackSocialViewModel()
Expand Down Expand Up @@ -147,6 +201,18 @@ class PrepublishingHomeFragment : Fragment(R.layout.post_prepublishing_home_frag
}
}

@Suppress("unused")
@Subscribe(threadMode = MAIN)
fun onMediaUploaded(event: OnMediaUploaded) {
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.markedLocallyAsFeatured && media.localPostId == getEditPostRepository().id) {
viewModel.refreshFeaturedImage()
}
}

companion object {
const val TAG = "prepublishing_home_fragment_tag"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading