Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* [**] Resolved an issue where the editor could become impossible to exit when it failed to load.
* [*] Atomic sites can now create application passwords without leaving the app.
* [**] Fixed a case where the editor failed to load on WP.com Atomic sites whose host doesn't expose `wp-block-editor/v1/settings`.
* [*] Try out the next-generation block editor on a per-site basis from Site Settings.

26.7
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.photopicker.MediaPickerConstants
import org.wordpress.android.ui.photopicker.MediaPickerLauncher
import org.wordpress.android.ui.posts.BasicDialogViewModel
import org.wordpress.android.ui.posts.GutenbergKitAnnouncementBottomSheetFragment
import org.wordpress.android.ui.posts.PostListType
import org.wordpress.android.ui.posts.PostUtils
import org.wordpress.android.ui.reader.ReaderActivityLauncher
Expand Down Expand Up @@ -402,6 +403,15 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
)
}

viewModel.onShowGutenbergKitAnnouncement.observeEvent(viewLifecycleOwner) { site ->
if (parentFragmentManager.isStateSaved) return@observeEvent
if (parentFragmentManager.findFragmentByTag(
GutenbergKitAnnouncementBottomSheetFragment.TAG
) != null) return@observeEvent
GutenbergKitAnnouncementBottomSheetFragment.newInstance(site)
.show(parentFragmentManager, GutenbergKitAnnouncementBottomSheetFragment.TAG)
}

viewModel.refresh.observe(viewLifecycleOwner) {
viewModel.refresh()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.mediapicker.MediaPickerActivity
import org.wordpress.android.ui.posts.BasicDialogViewModel
import org.wordpress.android.ui.posts.GutenbergEditorPreloader
import org.wordpress.android.ui.posts.GutenbergKitAnnouncementController
import org.wordpress.android.ui.sitecreation.misc.SiteCreationSource
import org.wordpress.android.util.BuildConfigWrapper
import org.wordpress.android.util.analytics.AnalyticsTrackerWrapper
Expand Down Expand Up @@ -68,11 +69,14 @@ class MySiteViewModel @Inject constructor(
private val siteCapabilityChecker: SiteCapabilityChecker,
private val gutenbergEditorPreloader: GutenbergEditorPreloader,
private val siteConnectivityBannerViewModelSlice: SiteConnectivityBannerViewModelSlice,
private val gutenbergKitAnnouncementController: GutenbergKitAnnouncementController,
) : ScopedViewModel(mainDispatcher) {
private val _onSnackbarMessage = MutableLiveData<Event<SnackbarMessageHolder>>()
private val _onNavigation = MutableLiveData<Event<SiteNavigationAction>>()
private val _onOpenJetpackInstallFullPluginOnboarding = SingleLiveEvent<Event<Unit>>()
private val _onShowJetpackIndividualPluginOverlay = SingleLiveEvent<Event<Unit>>()
private val _onShowGutenbergKitAnnouncement = SingleLiveEvent<Event<SiteModel>>()
val onShowGutenbergKitAnnouncement: LiveData<Event<SiteModel>> = _onShowGutenbergKitAnnouncement

/* Capture and track the site selected event so we can circumvent refreshing sources on resume
as they're already built on site select. */
Expand Down Expand Up @@ -185,6 +189,7 @@ class MySiteViewModel @Inject constructor(
fun onResume() {
isSiteSelected = false
checkAndShowJetpackFullPluginInstallOnboarding()
checkAndShowGutenbergKitAnnouncement()
selectedSiteRepository.updateSiteSettingsIfNecessary()
selectedSiteRepository.getSelectedSite()?.let {
buildDashboardOrSiteItems(it)
Expand All @@ -205,6 +210,14 @@ class MySiteViewModel @Inject constructor(
}
}

private fun checkAndShowGutenbergKitAnnouncement() {
selectedSiteRepository.getSelectedSite()?.let { selectedSite ->
if (gutenbergKitAnnouncementController.shouldShowAnnouncement(selectedSite)) {
_onShowGutenbergKitAnnouncement.postValue(Event(selectedSite))
}
}
}

fun onSiteNameChosen(input: String) {
siteInfoHeaderCardViewModelSlice.onSiteNameChosen(input)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class EditorCapabilityResolver @Inject constructor(
private val siteSettingsProvider: SiteSettingsProvider,
) {
fun resolveThirdPartyBlocks(site: SiteModel): EditorCapabilityState = when {
!gutenbergKitFeatureChecker.isGutenbergKitEnabled() -> EditorCapabilityState.Hidden
!gutenbergKitFeatureChecker.isGutenbergKitEnabled(site) -> EditorCapabilityState.Hidden
!gutenbergKitPluginsFeature.isEnabled() -> EditorCapabilityState.Hidden
!editorSettingsRepository.getSupportsEditorAssetsForSite(site) ->
EditorCapabilityState.Unsupported(EditorCapabilityState.UnsupportedReason.CapabilityMissing)
Expand All @@ -45,7 +45,7 @@ class EditorCapabilityResolver @Inject constructor(
}

fun resolveThemeStyles(site: SiteModel): EditorCapabilityState = when {
!gutenbergKitFeatureChecker.isGutenbergKitEnabled() -> EditorCapabilityState.Hidden
!gutenbergKitFeatureChecker.isGutenbergKitEnabled(site) -> EditorCapabilityState.Hidden
!editorSettingsRepository.getSupportsEditorSettingsForSite(site) ->
EditorCapabilityState.Unsupported(EditorCapabilityState.UnsupportedReason.CapabilityMissing)
else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ class EditorLauncher @Inject constructor(
* Determines if GutenbergKit editor should be used based on feature flags and post content.
*/
private fun shouldUseGutenbergKitEditor(params: EditorLauncherParams): Boolean {
val featureState = gutenbergKitFeatureChecker.getFeatureState()
val site = params.siteSource.getSite(siteStore)
val featureState = gutenbergKitFeatureChecker.getFeatureState(site)
val isGutenbergFeatureEnabled = featureState.isGutenbergKitEnabled

val site = params.siteSource.getSite(siteStore)
return when {
!isGutenbergFeatureEnabled -> {
logFeatureDisabledReason(featureState)
Expand Down Expand Up @@ -122,10 +122,10 @@ class EditorLauncher @Inject constructor(

private fun logFeatureDisabledReason(featureState: GutenbergKitFeatureChecker.FeatureState) {
val reason = when {
featureState.isDisableExperimentalBlockEditorEnabled ->
"the experimental block editor is explicitly disabled"
!featureState.isExperimentalBlockEditorEnabled && !featureState.isGutenbergKitFeatureEnabled ->
"neither the experimental block editor feature nor GutenbergKit feature is enabled"
featureState.siteOverride == false ->
"this site has an explicit GutenbergKit opt-out"
featureState.siteOverride == null && !featureState.isExperimentalBlockEditorEnabled ->
"no per-site opt-in and the experimental block editor flag is off"
else -> "GutenbergKit feature checks failed"
}
val featureFlags = getFeatureFlagsString(featureState)
Expand All @@ -144,8 +144,8 @@ class EditorLauncher @Inject constructor(
featureState: GutenbergKitFeatureChecker.FeatureState = gutenbergKitFeatureChecker.getFeatureState()
): String {
return "(experimental_block_editor: ${featureState.isExperimentalBlockEditorEnabled}, " +
"gutenberg_kit_feature: ${featureState.isGutenbergKitFeatureEnabled}, " +
"disable_experimental_block_editor: ${featureState.isDisableExperimentalBlockEditorEnabled})"
"gutenberg_kit_remote_flag: ${featureState.isGutenbergKitFeatureEnabled}, " +
"site_override: ${featureState.siteOverride})"
}

private fun logEditorDecision(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package org.wordpress.android.ui.posts

import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.WPWebViewActivity
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.util.extensions.getSerializableCompat
import javax.inject.Inject

/**
* One-time announcement bottom sheet for the upcoming GutenbergKit editor. Show/defer/activate
* logic lives in [GutenbergKitAnnouncementController]; this fragment hosts a Compose layout and
* forwards button taps.
*/
@AndroidEntryPoint
class GutenbergKitAnnouncementBottomSheetFragment : BottomSheetDialogFragment() {
@Inject lateinit var controller: GutenbergKitAnnouncementController

private var decisionRecorded = false

// The default `WordPress.BottomSheetDialogTheme` sets `fitsSystemWindows=true`, which adds
// the status-bar inset as top padding to the sheet container. The `NonTranslucent` variant
// turns that off — matches what other Compose bottom sheets (e.g. ReaderSubscriptionSettings)
// already do.
override fun getTheme(): Int = R.style.WordPress_BottomSheetDialogTheme_NonTranslucent

private val site: SiteModel
get() = requireNotNull(
requireArguments().getSerializableCompat<SiteModel>(WordPress.SITE)
) { "GutenbergKitAnnouncementBottomSheetFragment requires a SiteModel argument" }

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View = ComposeView(requireContext()).apply {
setContent {
AppThemeM3 {
GutenbergKitAnnouncementScreen(
onActivate = {
controller.onActivate(site)
decisionRecorded = true
dismiss()
},
onMaybeLater = {
controller.onMaybeLater(site)
decisionRecorded = true
dismiss()
},
onLearnMore = {
WPWebViewActivity.openURL(
requireContext(),
getString(R.string.gutenberg_kit_learn_more_url),
)
},
)
}
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Defer the expand to the show callback so the sheet slides up from offscreen instead of
// starting at its final position (which skips the slide-in animation entirely).
(dialog as? BottomSheetDialog)?.apply {
behavior.skipCollapsed = true
setOnShowListener {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
}

override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
// Swipe / back / tap-outside without a button tap is treated as an implicit "Maybe later"
// so the sheet doesn't re-prompt on the next My Site resume. Config changes don't count.
if (decisionRecorded) return
if (activity?.isChangingConfigurations == true) return
controller.onMaybeLater(site)
}

companion object {
const val TAG = "GutenbergKitAnnouncementBottomSheetFragment"

fun newInstance(site: SiteModel): GutenbergKitAnnouncementBottomSheetFragment =
GutenbergKitAnnouncementBottomSheetFragment().apply {
arguments = Bundle().apply { putSerializable(WordPress.SITE, site) }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package org.wordpress.android.ui.posts

import org.wordpress.android.datasets.SiteSettingsProvider
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import java.time.Clock
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

/**
* Owns the decisions for the GutenbergKit announcement bottom sheet and the per-site override
* it writes. Pure logic so it is unit-testable; the fragment and Site Settings only call into it.
*
* The per-site override is the single source of truth — its presence means the user has decided
* for that site (either direction), its absence means "not yet decided." "Maybe later" defers the
* announcement for one week per-site rather than writing an override, so we don't mis-read the
* user's intent.
*/
@Singleton
class GutenbergKitAnnouncementController @Inject constructor(
private val gutenbergKitFeatureChecker: GutenbergKitFeatureChecker,
private val siteSettingsProvider: SiteSettingsProvider,
private val appPrefsWrapper: AppPrefsWrapper,
private val clock: Clock,
) {
@Suppress("ReturnCount")
fun shouldShowAnnouncement(site: SiteModel): Boolean {
// The per-site override/deferral prefs are keyed by URL, so a site without one would
// loop the announcement on every resume (writes would no-op via TextUtils.isEmpty).
if (site.url.isNullOrEmpty()) return false
if (!gutenbergKitFeatureChecker.isGutenbergKitRemoteFeatureEnabled()) return false
if (!siteSettingsProvider.isBlockEditorDefault(site)) return false
if (appPrefsWrapper.getGutenbergKitSiteOverride(site.url) != null) return false
return clock.millis() >= appPrefsWrapper.getGutenbergKitAnnouncementDeferredUntil(site.url)
}

fun onActivate(site: SiteModel) = setOverride(site, true)

/**
* Records an explicit per-site decision (from the announcement sheet or Site Settings). An
* explicit decision supersedes any pending "Maybe later" deferral on the same site, so this
* clears the deferral timestamp as well.
*/
fun setOverride(site: SiteModel, enabled: Boolean) {
appPrefsWrapper.setGutenbergKitSiteOverride(site.url, enabled)
appPrefsWrapper.setGutenbergKitAnnouncementDeferredUntil(site.url, 0L)
}

fun onMaybeLater(site: SiteModel) {
appPrefsWrapper.setGutenbergKitAnnouncementDeferredUntil(
site.url,
clock.millis() + DEFER_DURATION_MILLIS
)
}

companion object {
val DEFER_DURATION_MILLIS: Long = TimeUnit.DAYS.toMillis(7)
}
}
Loading
Loading