Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
import org.wordpress.android.ui.photopicker.MediaPickerLauncher;
import org.wordpress.android.ui.posts.EditorConstants;
import org.wordpress.android.ui.posts.EditorLauncher;
import org.wordpress.android.ui.posts.GutenbergKitAnnouncementBottomSheetFragment;
import org.wordpress.android.ui.posts.PostUtils.EntryPoint;
import org.wordpress.android.ui.prefs.AppPrefs;
import org.wordpress.android.ui.prefs.AppSettingsActivity;
Expand Down Expand Up @@ -672,6 +673,11 @@ private void initViewModel() {
.show(getSupportFragmentManager(), FeatureAnnouncementDialogFragment.TAG);
});

mViewModel.getOnGutenbergKitAnnouncementRequested().observe(this, unused -> {
new GutenbergKitAnnouncementBottomSheetFragment()
.show(getSupportFragmentManager(), GutenbergKitAnnouncementBottomSheetFragment.TAG);
});

mFloatingActionButton.setOnClickListener(v -> {
PageType selectedPage = getSelectedPage();
if (selectedPage != null) mViewModel.onFabClicked(getSelectedSite(), selectedPage);
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package org.wordpress.android.ui.posts

import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import com.google.android.material.color.MaterialColors
import org.wordpress.android.R
import org.wordpress.android.ui.WPWebViewActivity
import org.wordpress.android.ui.prefs.AppPrefs

/**
* One-time announcement bottom sheet for the upcoming GutenbergKit editor.
* Sets the app-wide opt-in flag, or dismisses. Provides a "Learn more" link
* that opens a web page.
*/
class GutenbergKitAnnouncementBottomSheetFragment : BottomSheetDialogFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.gutenberg_kit_announcement_bottom_sheet, container, false)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

// Prevent Material's BottomSheetDialog from applying status-bar insets as top padding.
ViewCompat.setOnApplyWindowInsetsListener(view) { v, _ ->
v.setPadding(v.paddingLeft, 0, v.paddingRight, v.paddingBottom)
WindowInsetsCompat.CONSUMED
}

bindBodyWithLearnMore(view.findViewById(R.id.body_text))

view.findViewById<MaterialButton>(R.id.try_now_button).setOnClickListener {
AppPrefs.setGutenbergKitUserOptedIn(true)
dismiss()
}

view.findViewById<MaterialButton>(R.id.maybe_later_button).setOnClickListener {
dismiss()
}
}

private fun bindBodyWithLearnMore(textView: TextView) {
val body = getString(R.string.gutenberg_kit_announcement_body)
val learnMore = getString(R.string.gutenberg_kit_announcement_learn_more)
val combined = SpannableStringBuilder(body).append(' ').append(learnMore)
val start = combined.length - learnMore.length
val end = combined.length
val color = MaterialColors.getColor(textView, androidx.appcompat.R.attr.colorPrimary)
combined.setSpan(object : ClickableSpan() {
override fun onClick(widget: View) {
WPWebViewActivity.openURL(
requireContext(),
getString(R.string.gutenberg_kit_learn_more_url)
)
}

override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
}, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
combined.setSpan(ForegroundColorSpan(color), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = combined
textView.movementMethod = LinkMovementMethod.getInstance()
}

companion object {
const val TAG = "GutenbergKitAnnouncementBottomSheetFragment"
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.wordpress.android.ui.posts

import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures
import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures.Feature
import org.wordpress.android.util.config.GutenbergKitFeature
Expand All @@ -13,49 +15,58 @@ import javax.inject.Singleton
@Singleton
class GutenbergKitFeatureChecker @Inject constructor(
private val experimentalFeatures: ExperimentalFeatures,
private val gutenbergKitFeature: GutenbergKitFeature
private val gutenbergKitFeature: GutenbergKitFeature,
private val appPrefsWrapper: AppPrefsWrapper
) {
/**
* Data class containing the state of all GutenbergKit-related feature flags.
*/
data class FeatureState(
val isExperimentalBlockEditorEnabled: Boolean,
val isGutenbergKitFeatureEnabled: Boolean,
val isDisableExperimentalBlockEditorEnabled: Boolean
val isDisableExperimentalBlockEditorEnabled: Boolean,
val isUserOptedIn: Boolean = false,
val siteOverride: Boolean? = null
) {
/**
* Determines if GutenbergKit should be enabled based on the feature states.
*/
val isGutenbergKitEnabled: Boolean
get() = (isExperimentalBlockEditorEnabled || isGutenbergKitFeatureEnabled) &&
!isDisableExperimentalBlockEditorEnabled
get() {
val perUserOrSite = siteOverride ?: isUserOptedIn
return (isExperimentalBlockEditorEnabled ||
isGutenbergKitFeatureEnabled ||
perUserOrSite) &&
!isDisableExperimentalBlockEditorEnabled
}
}

/**
* Gets the current state of all GutenbergKit-related feature flags.
*
* @return FeatureState containing all flag states and the computed enabled state
* Gets the current state of all GutenbergKit-related feature flags for the given site (if any).
*/
fun getFeatureState(): FeatureState {
@JvmOverloads
fun getFeatureState(site: SiteModel? = null): FeatureState {
return FeatureState(
isExperimentalBlockEditorEnabled = experimentalFeatures.isEnabled(Feature.EXPERIMENTAL_BLOCK_EDITOR),
isGutenbergKitFeatureEnabled = gutenbergKitFeature.isEnabled(),
isDisableExperimentalBlockEditorEnabled = experimentalFeatures.isEnabled(
Feature.DISABLE_EXPERIMENTAL_BLOCK_EDITOR
)
),
isUserOptedIn = appPrefsWrapper.isGutenbergKitUserOptedIn,
siteOverride = site?.url?.let { appPrefsWrapper.getGutenbergKitSiteOverride(it) }
)
}

/**
* Determines if GutenbergKit is enabled based on feature flags.
*
* The feature is enabled if:
* - Either the experimental block editor is enabled OR the GutenbergKit feature flag is on
* - AND the disable experimental block editor flag is NOT enabled
*
* @return true if GutenbergKit should be enabled, false otherwise
* Determines if GutenbergKit is enabled based on feature flags (and optional per-site opt-in).
*/
fun isGutenbergKitEnabled(): Boolean {
return getFeatureState().isGutenbergKitEnabled
@JvmOverloads
fun isGutenbergKitEnabled(site: SiteModel? = null): Boolean {
return getFeatureState(site).isGutenbergKitEnabled
}

/**
* Whether the user-facing remote feature flag is on (controls opt-in surfaces).
*/
fun isGutenbergKitRemoteFeatureEnabled(): Boolean = gutenbergKitFeature.isEnabled()
}
119 changes: 119 additions & 0 deletions WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public enum DeletablePrefKey implements PrefKey {
SHOULD_AUTO_ENABLE_GUTENBERG_FOR_THE_NEW_POSTS_PHASE_2,
GUTENBERG_OPT_IN_DIALOG_SHOWN,
GUTENBERG_FOCAL_POINT_PICKER_TOOLTIP_SHOWN,
GUTENBERG_KIT_OPT_IN_SITES,
GUTENBERG_KIT_OPT_OUT_SITES,

POST_LIST_AUTHOR_FILTER,
POST_LIST_VIEW_LAYOUT_TYPE,
Expand Down Expand Up @@ -322,6 +324,9 @@ public enum UndeletablePrefKey implements PrefKey {
// These preferences persist across logout/login cycles.
IS_TRACK_NETWORK_REQUESTS_ENABLED,
TRACK_NETWORK_REQUESTS_RETENTION_PERIOD,

GUTENBERG_KIT_ANNOUNCEMENT_SHOWN,
GUTENBERG_KIT_USER_OPT_IN,
}

static SharedPreferences prefs() {
Expand Down Expand Up @@ -831,6 +836,120 @@ public static boolean isGutenbergInfoPopupDisplayed(String siteURL) {
return urls != null && urls.contains(siteURL);
}

/**
* Returns the explicit per-site override for GutenbergKit, or {@code null} if the user has
* not set one (in which case the global opt-in flag should be used).
*/
@Nullable
public static Boolean getGutenbergKitSiteOverride(String siteURL) {
if (TextUtils.isEmpty(siteURL)) {
return null;
}
if (siteSetContains(DeletablePrefKey.GUTENBERG_KIT_OPT_OUT_SITES, siteURL)) {
return Boolean.FALSE;
}
if (siteSetContains(DeletablePrefKey.GUTENBERG_KIT_OPT_IN_SITES, siteURL)) {
return Boolean.TRUE;
}
return null;
}

/**
* Sets an explicit per-site override for GutenbergKit, replacing any prior override for the
* site. The site is added to the opt-in or opt-out set (per {@code enabled}) and removed from
* the other so the two sets stay mutually exclusive.
*/
public static void setGutenbergKitSiteOverride(String siteURL, boolean enabled) {
if (TextUtils.isEmpty(siteURL)) {
return;
}
DeletablePrefKey added = enabled
? DeletablePrefKey.GUTENBERG_KIT_OPT_IN_SITES
: DeletablePrefKey.GUTENBERG_KIT_OPT_OUT_SITES;
DeletablePrefKey removed = enabled
? DeletablePrefKey.GUTENBERG_KIT_OPT_OUT_SITES
: DeletablePrefKey.GUTENBERG_KIT_OPT_IN_SITES;
addToSiteSet(added, siteURL);
removeFromSiteSet(removed, siteURL);
}

/**
* Returns {@code true} if {@code siteURL} is currently a member of the StringSet at {@code key}.
* A missing entry or a value of the wrong type is treated as absence.
*/
private static boolean siteSetContains(DeletablePrefKey key, String siteURL) {
try {
Set<String> urls = prefs().getStringSet(key.name(), null);
return urls != null && urls.contains(siteURL);
} catch (ClassCastException exp) {
return false;
}
}

/**
* Adds {@code siteURL} to the StringSet at {@code key}, creating the set if it does not exist.
* No-ops if the stored value is of the wrong type.
*/
private static void addToSiteSet(DeletablePrefKey key, String siteURL) {
Set<String> urls;
try {
urls = prefs().getStringSet(key.name(), null);
} catch (ClassCastException exp) {
return;
}
Set<String> newUrls = new HashSet<>();
if (urls != null) newUrls.addAll(urls);
newUrls.add(siteURL);
prefs().edit().putStringSet(key.name(), newUrls).apply();
}

/**
* Removes {@code siteURL} from the StringSet at {@code key}. No-ops if the site is not present
* or the stored value is of the wrong type.
*/
private static void removeFromSiteSet(DeletablePrefKey key, String siteURL) {
Set<String> urls;
try {
urls = prefs().getStringSet(key.name(), null);
} catch (ClassCastException exp) {
return;
}
if (urls == null || !urls.contains(siteURL)) return;
Set<String> newUrls = new HashSet<>(urls);
newUrls.remove(siteURL);
prefs().edit().putStringSet(key.name(), newUrls).apply();
}

/**
* Returns {@code true} if the user has opted into GutenbergKit app-wide via the announcement
* sheet. The opt-in is the baseline that per-site overrides modify.
*/
public static boolean isGutenbergKitUserOptedIn() {
return prefs().getBoolean(UndeletablePrefKey.GUTENBERG_KIT_USER_OPT_IN.name(), false);
}

/**
* Sets the app-wide GutenbergKit opt-in. Persists across logout (undeletable pref).
*/
public static void setGutenbergKitUserOptedIn(boolean optedIn) {
prefs().edit().putBoolean(UndeletablePrefKey.GUTENBERG_KIT_USER_OPT_IN.name(), optedIn).apply();
}

/**
* Returns {@code true} if the GutenbergKit announcement bottom sheet has been presented to
* this user. Used to ensure the announcement is shown at most once.
*/
public static boolean wasGutenbergKitAnnouncementShown() {
return prefs().getBoolean(UndeletablePrefKey.GUTENBERG_KIT_ANNOUNCEMENT_SHOWN.name(), false);
}

/**
* Records whether the GutenbergKit announcement bottom sheet has been shown.
*/
public static void setGutenbergKitAnnouncementShown(boolean shown) {
prefs().edit().putBoolean(UndeletablePrefKey.GUTENBERG_KIT_ANNOUNCEMENT_SHOWN.name(), shown).apply();
}

public static void setGutenbergInfoPopupDisplayed(String siteURL, boolean isDisplayed) {
if (isGutenbergInfoPopupDisplayed(siteURL)) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,20 @@ class AppPrefsWrapper @Inject constructor(val buildConfigWrapper: BuildConfigWra
fun setStatsNewStatsSuggestionLastDismissedAt(timestamp: Long) =
AppPrefs.setStatsNewStatsSuggestionLastDismissedAt(timestamp)

fun getGutenbergKitSiteOverride(siteUrl: String?): Boolean? =
AppPrefs.getGutenbergKitSiteOverride(siteUrl)

fun setGutenbergKitSiteOverride(siteUrl: String?, enabled: Boolean) =
AppPrefs.setGutenbergKitSiteOverride(siteUrl, enabled)

var isGutenbergKitUserOptedIn: Boolean
get() = AppPrefs.isGutenbergKitUserOptedIn()
set(value) = AppPrefs.setGutenbergKitUserOptedIn(value)

var wasGutenbergKitAnnouncementShown: Boolean
get() = AppPrefs.wasGutenbergKitAnnouncementShown()
set(value) = AppPrefs.setGutenbergKitAnnouncementShown(value)

companion object {
private const val LIGHT_MODE_ID = 0
private const val DARK_MODE_ID = 1
Expand Down
Loading
Loading