diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorConfigurationBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorConfigurationBuilder.kt index a1bea3d546b0..98912093b939 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorConfigurationBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/EditorConfigurationBuilder.kt @@ -1,7 +1,8 @@ package org.wordpress.android.ui.posts import org.wordpress.android.util.UrlUtils -import org.wordpress.gutenberg.EditorConfiguration +import org.wordpress.gutenberg.model.EditorConfiguration +import org.wordpress.gutenberg.model.PostTypeDetails /** * Utility object for building EditorConfiguration from settings maps. @@ -12,47 +13,64 @@ object EditorConfigurationBuilder { * Builds an EditorConfiguration from the provided settings map. * * @param settings The settings map containing all configuration values - * @param editorSettings Optional editor settings string (null for warmup scenarios) * @return Configured EditorConfiguration instance */ fun build( settings: Map, - editorSettings: String? = null ): EditorConfiguration { - return EditorConfiguration.Builder().apply { - val postId = settings.getSetting("postId")?.let { if (it == 0) -1 else it } - val siteURL = settings.getSetting("siteURL") ?: "" - val siteApiNamespace = settings.getStringArray("siteApiNamespace") + val siteURL = settings.getSetting("siteURL") ?: "" + val siteApiRoot = settings.getSetting("siteApiRoot") ?: "" + val postType = settings.getSetting("postType") + ?: PostTypeDetails.post + val siteApiNamespace = settings.getStringArray("siteApiNamespace") + + return EditorConfiguration.builder( + siteURL = siteURL, + siteApiRoot = siteApiRoot, + postType = postType + ).apply { + val postId = settings.getSetting("postId") + ?.let { if (it == 0) null else it.toUInt() } // Post settings setTitle(settings.getSetting("postTitle") ?: "") setContent(settings.getSetting("postContent") ?: "") setPostId(postId) - setPostType(settings.getSetting("postType")) + setPostStatus(settings.getSetting("status") ?: "draft") // Site settings - setSiteURL(siteURL) - setSiteApiRoot(settings.getSetting("siteApiRoot") ?: "") setSiteApiNamespace(siteApiNamespace) - setNamespaceExcludedPaths(settings.getStringArray("namespaceExcludedPaths")) - setAuthHeader(settings.getSetting("authHeader") ?: "") + setNamespaceExcludedPaths( + settings.getStringArray("namespaceExcludedPaths") + ) + setAuthHeader( + settings.getSetting("authHeader") ?: "" + ) // Features - setThemeStyles(settings.getSettingOrDefault("themeStyles", false)) - setPlugins(settings.getSettingOrDefault("plugins", false)) + setThemeStyles( + settings.getSettingOrDefault("themeStyles", false) + ) + setPlugins( + settings.getSettingOrDefault("plugins", false) + ) setLocale(settings.getSetting("locale") ?: "en") // Editor asset caching configuration - configureEditorAssetCaching(settings, siteURL, siteApiNamespace) + configureEditorAssetCaching( + settings, siteURL, siteApiNamespace + ) // Cookies - setCookies(settings.getSetting>("cookies") ?: emptyMap()) + setCookies( + settings.getSetting>("cookies") + ?: emptyMap() + ) // Network logging for debugging - setEnableNetworkLogging(settings.getSettingOrDefault("enableNetworkLogging", false)) - - // Editor settings (null for warmup scenarios) - setEditorSettings(editorSettings) + setEnableNetworkLogging( + settings.getSettingOrDefault("enableNetworkLogging", false) + ) }.build() } @@ -72,18 +90,28 @@ object EditorConfigurationBuilder { setCachedAssetHosts(cachedHosts) val firstNamespace = siteApiNamespace.firstOrNull() ?: "" - val siteApiRoot = settings.getSetting("siteApiRoot") ?: "" + val siteApiRoot = + settings.getSetting("siteApiRoot") ?: "" if (firstNamespace.isNotEmpty() && siteApiRoot.isNotEmpty()) { - setEditorAssetsEndpoint("${siteApiRoot}wpcom/v2/${firstNamespace}editor-assets") + setEditorAssetsEndpoint( + "${siteApiRoot}wpcom/v2/${firstNamespace}editor-assets" + ) } } - // Type-safe settings accessors - moved from GutenbergKitEditorFragment - private inline fun Map.getSetting(key: String): T? = this[key] as? T + // Type-safe settings accessors + private inline fun Map.getSetting( + key: String + ): T? = this[key] as? T - private inline fun Map.getSettingOrDefault(key: String, default: T): T = - getSetting(key) ?: default + private inline fun Map.getSettingOrDefault( + key: String, default: T + ): T = getSetting(key) ?: default - private fun Map.getStringArray(key: String): Array = - getSetting>(key)?.asSequence()?.filterNotNull()?.toList()?.toTypedArray() ?: emptyArray() + private fun Map.getStringArray( + key: String + ): Array = + getSetting>(key) + ?.asSequence()?.filterNotNull()?.toList()?.toTypedArray() + ?: emptyArray() } 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 1ebf3fb74dd4..6c6ee25e5a86 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 @@ -2209,40 +2209,35 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene } val siteConfig = GutenbergKitSettingsBuilder.SiteConfig.fromSiteModel(siteModel) - val postConfig = GutenbergKitSettingsBuilder.PostConfig.fromPostModel( editPostRepository.getPost() ) - val featureConfig = GutenbergKitSettingsBuilder.FeatureConfig( isPluginsFeatureEnabled = gutenbergKitPluginsFeature.isEnabled(), isThemeStylesFeatureEnabled = siteSettings?.useThemeStyles ?: true, isNetworkLoggingEnabled = AppPrefs.isTrackNetworkRequestsEnabled() ) - val appConfig = GutenbergKitSettingsBuilder.AppConfig( accessToken = accountStore.accessToken, locale = perAppLocaleManager.getCurrentLocaleLanguageCode(), - cookies = editPostAuthViewModel.getCookiesForPrivateSites(site, privateAtomicCookie), + cookies = editPostAuthViewModel.getCookiesForPrivateSites( + site, privateAtomicCookie + ), accountUserId = accountStore.account.userId, accountUserName = accountStore.account.userName, userAgent = userAgent, isJetpackSsoEnabled = isJetpackSsoEnabled ) - val config = GutenbergKitSettingsBuilder.GutenbergKitConfig( + val settings = GutenbergKitSettingsBuilder.buildSettings( siteConfig = siteConfig, postConfig = postConfig, appConfig = appConfig, featureConfig = featureConfig ) + val configuration = EditorConfigurationBuilder.build(settings) - return GutenbergKitEditorFragment.newInstanceWithBuilder( - getContext(), - isNewPost, - jetpackFeatureRemovalPhaseHelper.shouldShowJetpackPoweredEditorFeatures(), - config - ) + return GutenbergKitEditorFragment.newInstance(configuration) } override fun instantiateItem(container: ViewGroup, position: Int): Any { @@ -2955,6 +2950,10 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene Handler(Looper.getMainLooper()).post { invalidateOptionsMenu() } } + override fun getPersistedTitle(): String = editPostRepository.title + + override fun getPersistedContent(): String = editPostRepository.content + // FluxC events @Suppress("unused", "CyclomaticComplexMethod") @Subscribe(threadMode = ThreadMode.MAIN) @@ -3133,8 +3132,9 @@ class GutenbergKitActivity : BaseAppCompatActivity(), EditorImageSettingsListene @Suppress("unused") @Subscribe(threadMode = ThreadMode.MAIN_ORDERED) fun onEditorSettingsChanged(event: OnEditorSettingsChanged) { - val editorSettingsString = event.editorSettings?.toJsonString() ?: "undefined" - editorFragment?.startWithEditorSettings(editorSettingsString) + // In GutenbergKit v0.15.0, the editor configuration is provided at + // construction time. Editor settings are no longer applied after the + // fact. This handler is retained for the EventBus subscription. } // EditorDataProvider methods diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilder.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilder.kt index 8e43e1eb2891..d35fff16f41f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilder.kt @@ -9,6 +9,7 @@ import org.wordpress.android.fluxc.utils.extensions.getPasswordProcessed import org.wordpress.android.fluxc.utils.extensions.getUserNameProcessed import org.wordpress.android.util.AppLog import org.wordpress.android.util.UrlUtils +import org.wordpress.gutenberg.model.PostTypeDetails object GutenbergKitSettingsBuilder { private const val AUTH_BEARER_PREFIX = "Bearer " @@ -54,7 +55,8 @@ object GutenbergKitSettingsBuilder { val remotePostId: Long?, val isPage: Boolean, val title: String?, - val content: String? + val content: String?, + val status: String? ) { companion object { fun fromPostModel(postModel: PostImmutableModel?): PostConfig { @@ -62,7 +64,8 @@ object GutenbergKitSettingsBuilder { remotePostId = postModel?.remotePostId, isPage = postModel?.isPage ?: false, title = postModel?.title, - content = postModel?.content + content = postModel?.content, + status = postModel?.status ) } } @@ -126,7 +129,12 @@ object GutenbergKitSettingsBuilder { return mutableMapOf( "postId" to postConfig.remotePostId?.toInt(), - "postType" to if (postConfig.isPage) "page" else "post", + "postType" to if (postConfig.isPage) { + PostTypeDetails.page + } else { + PostTypeDetails.post + }, + "status" to postConfig.status, "postTitle" to postConfig.title, "postContent" to postConfig.content, "siteURL" to siteConfig.url, diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitWarmupHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitWarmupHelper.kt index f84f326fb1f7..1595c640a965 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitWarmupHelper.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/GutenbergKitWarmupHelper.kt @@ -1,20 +1,13 @@ package org.wordpress.android.ui.posts -import android.content.Context import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.network.UserAgent -import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.modules.BG_THREAD import org.wordpress.android.util.AppLog import org.wordpress.android.util.AppLog.T -import org.wordpress.android.util.PerAppLocaleManager import org.wordpress.android.util.SiteUtils -import org.wordpress.android.util.config.GutenbergKitPluginsFeature -import org.wordpress.gutenberg.EditorConfiguration -import org.wordpress.gutenberg.GutenbergView import javax.inject.Inject import javax.inject.Named import javax.inject.Singleton @@ -25,12 +18,7 @@ import javax.inject.Singleton */ @Singleton class GutenbergKitWarmupHelper @Inject constructor( - private val appContext: Context, - private val accountStore: AccountStore, - private val userAgent: UserAgent, - private val perAppLocaleManager: PerAppLocaleManager, private val gutenbergKitFeatureChecker: GutenbergKitFeatureChecker, - private val gutenbergKitPluginsFeature: GutenbergKitPluginsFeature, @Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher ) { private var lastWarmedUpSiteId: Long? = null @@ -93,62 +81,13 @@ class GutenbergKitWarmupHelper @Inject constructor( return shouldWarmup } + @Suppress("UnusedParameter") private suspend fun performWarmup(site: SiteModel) { - try { - isWarmupInProgress = true - AppLog.d(T.EDITOR, "GutenbergKitWarmupHelper: Starting warmup for site ${site.siteId}") - - val configuration = buildWarmupConfiguration(site) - - // Perform the warmup on the main thread as it involves WebView - kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) { - GutenbergView.warmup(appContext, configuration) - } - - lastWarmedUpSiteId = site.siteId - AppLog.d(T.EDITOR, "GutenbergKitWarmupHelper: Warmup completed for site ${site.siteId}") - } catch (e: IllegalStateException) { - AppLog.e(T.EDITOR, "GutenbergKitWarmupHelper: Warmup failed - illegal state", e) - } finally { - isWarmupInProgress = false - } - } - - private fun buildWarmupConfiguration(site: SiteModel): EditorConfiguration { - // Build the configuration using the same patterns as GutenbergKitSettingsBuilder - val siteConfig = GutenbergKitSettingsBuilder.SiteConfig.fromSiteModel(site) - - // Create minimal post config for warmup (no specific post data) - val postConfig = GutenbergKitSettingsBuilder.PostConfig( - remotePostId = null, - isPage = false, - title = "", - content = "" + // GutenbergView.warmup() was removed in GutenbergKit v0.15.0. + // Warmup/preloading needs to be reimplemented using the new API. + AppLog.d( + T.EDITOR, + "GutenbergKitWarmupHelper: Warmup not yet supported in v0.15.0" ) - - val appConfig = GutenbergKitSettingsBuilder.AppConfig( - accessToken = accountStore.accessToken, - locale = perAppLocaleManager.getCurrentLocaleLanguageCode(), - cookies = null, // No cookies needed for warmup - accountUserId = accountStore.account.userId, - accountUserName = accountStore.account.userName, - userAgent = userAgent, - isJetpackSsoEnabled = false // Default to false for warmup - ) - - val featureConfig = GutenbergKitSettingsBuilder.FeatureConfig( - isPluginsFeatureEnabled = gutenbergKitPluginsFeature.isEnabled(), - // Default to true during warmup; actual value will be used when editor launches - isThemeStylesFeatureEnabled = true - ) - - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = siteConfig, - postConfig = postConfig, - appConfig = appConfig, - featureConfig = featureConfig - ) - - return EditorConfigurationBuilder.build(settings, editorSettings = null) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragment.kt index eb865c9803ab..9c56674e6b5f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragment.kt @@ -13,6 +13,8 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.webkit.URLUtil +import android.widget.FrameLayout +import androidx.core.os.BundleCompat import androidx.core.util.Pair import androidx.lifecycle.LiveData import androidx.lifecycle.lifecycleScope @@ -24,15 +26,10 @@ import org.wordpress.android.editor.EditorEditMediaListener import org.wordpress.android.editor.EditorFragmentAbstract import org.wordpress.android.editor.EditorImagePreviewListener import org.wordpress.android.editor.LiveTextWatcher -import org.wordpress.android.editor.gutenberg.GutenbergWebViewAuthorizationData -import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase.Companion.getDatabase -import org.wordpress.android.ui.posts.EditorConfigurationBuilder -import org.wordpress.android.ui.posts.GutenbergKitSettingsBuilder import org.wordpress.android.util.AppLog import org.wordpress.android.util.PermissionUtils import org.wordpress.android.util.ProfilingUtils import org.wordpress.android.util.helpers.MediaFile -import org.wordpress.gutenberg.EditorConfiguration import org.wordpress.gutenberg.GutenbergView import org.wordpress.gutenberg.GutenbergView.ContentChangeListener import org.wordpress.gutenberg.GutenbergView.FeaturedImageChangeListener @@ -41,7 +38,7 @@ import org.wordpress.gutenberg.GutenbergView.LogJsExceptionListener import org.wordpress.gutenberg.GutenbergView.OpenMediaLibraryListener import org.wordpress.gutenberg.GutenbergView.TitleAndContentCallback import org.wordpress.gutenberg.Media -import java.io.Serializable +import org.wordpress.gutenberg.model.EditorConfiguration import java.util.concurrent.CountDownLatch class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { @@ -55,9 +52,6 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { private var onLogJsExceptionListener: LogJsExceptionListener? = null private var modalDialogStateListener: GutenbergView.ModalDialogStateListener? = null private var networkRequestListener: GutenbergView.NetworkRequestListener? = null - - private var editorStarted = false - private var isEditorDidMount = false private var rootView: View? = null private var isXPostsEnabled: Boolean = false @@ -69,8 +63,6 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { if (savedInstanceState != null) { isHtmlModeEnabled = savedInstanceState.getBoolean(KEY_HTML_MODE_ENABLED) - editorStarted = savedInstanceState.getBoolean(KEY_EDITOR_STARTED) - isEditorDidMount = savedInstanceState.getBoolean(KEY_EDITOR_DID_MOUNT) mFeaturedImageId = savedInstanceState.getLong(ARG_FEATURED_IMAGE_ID) } } @@ -93,34 +85,41 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { // Set up media library listener openMediaLibraryListener = object : OpenMediaLibraryListener { - override fun onOpenMediaLibrary(config: GutenbergView.OpenMediaLibraryConfig) { + override fun onOpenMediaLibrary( + config: GutenbergView.OpenMediaLibraryConfig + ) { mEditorFragmentListener.onOpenMediaLibraryRequested(config) } } // Set up JS exception listener onLogJsExceptionListener = object : LogJsExceptionListener { - override fun onLogJsException(exception: org.wordpress.gutenberg.GutenbergJsException) { + override fun onLogJsException( + exception: org.wordpress.gutenberg.GutenbergJsException + ) { val stackTraceElements = exception.stackTrace.map { stackTrace -> - com.automattic.android.tracks.crashlogging.JsExceptionStackTraceElement( - stackTrace.fileName, - stackTrace.lineNumber, - stackTrace.colNumber, - stackTrace.function - ) + com.automattic.android.tracks.crashlogging + .JsExceptionStackTraceElement( + stackTrace.fileName, + stackTrace.lineNumber, + stackTrace.colNumber, + stackTrace.function + ) } - val jsException = com.automattic.android.tracks.crashlogging.JsException( - exception.type, - exception.message, - stackTraceElements, - exception.context, - exception.tags, - exception.isHandled, - exception.handledBy - ) + val jsException = + com.automattic.android.tracks.crashlogging.JsException( + exception.type, + exception.message, + stackTraceElements, + exception.context, + exception.tags, + exception.isHandled, + exception.handledBy + ) - val callback = object : com.automattic.android.tracks.crashlogging.JsExceptionCallback { + val callback = object : + com.automattic.android.tracks.crashlogging.JsExceptionCallback { override fun onReportSent(sent: Boolean) { // Do nothing } @@ -142,90 +141,136 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { } } + @Suppress("LongMethod") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - if (arguments != null) { - @Suppress("UNCHECKED_CAST", "DEPRECATION") - settings = requireArguments().getSerializable(ARG_GUTENBERG_KIT_SETTINGS) as Map? - } - // Set up fragment's own listeners before initializing the editor initializeFragmentListeners() mEditorFragmentListener.onEditorFragmentInitialized() - rootView = inflater.inflate(R.layout.fragment_gutenberg_kit_editor, container, false) - val gutenbergViewContainer = rootView!!.findViewById(R.id.gutenberg_view_container) - - gutenbergView = GutenbergView.createForEditor(requireContext()).also { gutenbergView -> - gutenbergView.layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT + rootView = inflater.inflate( + R.layout.fragment_gutenberg_kit_editor, container, false + ) + val gutenbergViewContainer = + rootView!!.findViewById(R.id.gutenberg_view_container) + + val configuration = requireNotNull( + BundleCompat.getParcelable( + requireArguments(), + ARG_GUTENBERG_KIT_SETTINGS, + EditorConfiguration::class.java + ) + ) + + val gutenbergView = GutenbergView( + configuration = configuration, + dependencies = null, + coroutineScope = this.lifecycleScope, + context = requireContext() + ) + + gutenbergViewContainer.addView( + gutenbergView, + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT ) - gutenbergViewContainer.addView(gutenbergView) + ) - gutenbergView.setOnFileChooserRequestedListener { intent: Intent?, requestCode: Int? -> - @Suppress("DEPRECATION") startActivityForResult(intent!!, requestCode!!) - null + gutenbergView.setOnFileChooserRequestedListener { + intent: Intent?, requestCode: Int? -> + @Suppress("DEPRECATION") + startActivityForResult(intent!!, requestCode!!) + null + } + gutenbergView.setContentChangeListener(object : ContentChangeListener { + override fun onContentChanged() { + textWatcher.postTextChanged() } - gutenbergView.setContentChangeListener(object : ContentChangeListener { - override fun onContentChanged() { - textWatcher.postTextChanged() + }) + historyChangeListener?.let(gutenbergView::setHistoryChangeListener) + featuredImageChangeListener?.let( + gutenbergView::setFeaturedImageChangeListener + ) + openMediaLibraryListener?.let( + gutenbergView::setOpenMediaLibraryListener + ) + onLogJsExceptionListener?.let( + gutenbergView::setLogJsExceptionListener + ) + modalDialogStateListener?.let( + gutenbergView::setModalDialogStateListener + ) + networkRequestListener?.let( + gutenbergView::setNetworkRequestListener + ) + + // Set up content provider for WebView refresh recovery + gutenbergView.setLatestContentProvider( + object : GutenbergView.LatestContentProvider { + override fun getLatestContent(): GutenbergView.LatestContent { + return GutenbergView.LatestContent( + mEditorFragmentListener.persistedTitle, + mEditorFragmentListener.persistedContent + ) } - }) - historyChangeListener?.let(gutenbergView::setHistoryChangeListener) - featuredImageChangeListener?.let(gutenbergView::setFeaturedImageChangeListener) - openMediaLibraryListener?.let(gutenbergView::setOpenMediaLibraryListener) - onLogJsExceptionListener?.let(gutenbergView::setLogJsExceptionListener) - modalDialogStateListener?.let(gutenbergView::setModalDialogStateListener) - networkRequestListener?.let(gutenbergView::setNetworkRequestListener) - - // Set up autocomplete listener for user mentions and cross-post suggestions - gutenbergView.setAutocompleterTriggeredListener(object : GutenbergView.AutocompleterTriggeredListener { + } + ) + + // Set up autocomplete listener for user mentions and cross-post suggestions + gutenbergView.setAutocompleterTriggeredListener( + object : GutenbergView.AutocompleterTriggeredListener { override fun onAutocompleterTriggered(type: String) { when (type) { - "at-symbol" -> mEditorFragmentListener.showUserSuggestions { result -> - result?.let { - // Appended space completes the autocomplete session - gutenbergView.appendTextAtCursor("$it ") + "at-symbol" -> + mEditorFragmentListener.showUserSuggestions { result -> + result?.let { + // Appended space completes the autocomplete session + gutenbergView.appendTextAtCursor("$it ") + } } - } "plus-symbol" -> { if (isXPostsEnabled) { - mEditorFragmentListener.showXpostSuggestions { result -> - result?.let { - // Appended space completes the autocomplete session - gutenbergView.appendTextAtCursor("$it ") + mEditorFragmentListener + .showXpostSuggestions { result -> + result?.let { + // Appended space completes the autocomplete session + gutenbergView + .appendTextAtCursor("$it ") + } } - } } } } } - }) - - gutenbergView.setEditorDidBecomeAvailable { - isEditorDidMount = true - mEditorFragmentListener.onEditorFragmentContentReady(ArrayList(), false) - setEditorProgressBarVisibility(false) } + ) + + gutenbergView.setEditorDidBecomeAvailable { + mEditorFragmentListener.onEditorFragmentContentReady( + ArrayList(), false + ) } - setEditorProgressBarVisibility(true) + this.gutenbergView = gutenbergView return rootView } override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - gutenbergView?.invalidate() } @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - @Suppress("DEPRECATION") super.onActivityResult(requestCode, resultCode, data) + override fun onActivityResult( + requestCode: Int, resultCode: Int, data: Intent? + ) { + @Suppress("DEPRECATION") + super.onActivityResult(requestCode, resultCode, data) gutenbergView?.let { gutenbergView -> if (requestCode == gutenbergView.pickImageRequestCode) { @@ -234,7 +279,9 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { } } - private fun handleFileChooserResult(gutenbergView: GutenbergView, resultCode: Int, data: Intent?) { + private fun handleFileChooserResult( + gutenbergView: GutenbergView, resultCode: Int, data: Intent? + ) { val filePathCallback = gutenbergView.filePathCallback ?: return if (resultCode != Activity.RESULT_OK) { @@ -245,23 +292,13 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { lifecycleScope.launch { val uris = gutenbergView.extractUrisFromIntent(data) - val processedUris = gutenbergView.processFileUris(requireContext(), uris) + val processedUris = + gutenbergView.processFileUris(requireContext(), uris) filePathCallback.onReceiveValue(processedUris) gutenbergView.resetFilePathCallback() } } - override fun onResume() { - super.onResume() - setEditorProgressBarVisibility(!isEditorDidMount) - } - - private fun setEditorProgressBarVisibility(shown: Boolean) { - if (isAdded) { - rootView?.findViewById(R.id.editor_progress).setVisibleOrGone(shown) - } - } - @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") override fun onRequestPermissionsResult( @@ -280,40 +317,42 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { super.onAttach(context) val activity = context as Activity - mEditorImagePreviewListener = requireActivityImplements(activity) - mEditorEditMediaListener = requireActivityImplements(activity) + mEditorImagePreviewListener = + requireActivityImplements(activity) + mEditorEditMediaListener = + requireActivityImplements(activity) } - private inline fun requireActivityImplements(activity: Activity): T? { + private inline fun requireActivityImplements( + activity: Activity + ): T? { return try { activity as T? } catch (e: ClassCastException) { - throw ClassCastException("$activity must implement ${T::class.simpleName}: $e") + throw ClassCastException( + "$activity must implement ${T::class.simpleName}: $e" + ) } } - // View extension functions - private fun View?.setVisibleOrGone(visible: Boolean) { - this?.visibility = if (visible) View.VISIBLE else View.GONE - } - override fun onSaveInstanceState(outState: Bundle) { outState.putBoolean(KEY_HTML_MODE_ENABLED, isHtmlModeEnabled) - outState.putBoolean(KEY_EDITOR_STARTED, editorStarted) - outState.putBoolean(KEY_EDITOR_DID_MOUNT, isEditorDidMount) outState.putLong(ARG_FEATURED_IMAGE_ID, mFeaturedImageId) } @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(org.wordpress.android.editor.R.menu.menu_gutenberg, menu) + inflater.inflate( + org.wordpress.android.editor.R.menu.menu_gutenberg, menu + ) } @Deprecated("Deprecated in Java") @Suppress("DEPRECATION") override fun onPrepareOptionsMenu(menu: Menu) { - val debugMenuItem = menu.findItem(org.wordpress.android.editor.R.id.debugmenu) + val debugMenuItem = + menu.findItem(org.wordpress.android.editor.R.id.debugmenu) debugMenuItem.isVisible = BuildConfig.DEBUG @Suppress("DEPRECATION") super.onPrepareOptionsMenu(menu) @@ -348,13 +387,17 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { private fun toggleHtmlMode() { isHtmlModeEnabled = !isHtmlModeEnabled - mEditorFragmentListener.onTrackableEvent(EditorFragmentAbstract.TrackableEvent.HTML_BUTTON_TAPPED) + mEditorFragmentListener.onTrackableEvent( + EditorFragmentAbstract.TrackableEvent.HTML_BUTTON_TAPPED + ) mEditorFragmentListener.onHtmlModeToggledInToolbar() gutenbergView?.textEditorEnabled = isHtmlModeEnabled } @Throws(EditorFragmentNotAddedException::class) - override fun getTitleAndContent(originalContent: CharSequence): Pair { + override fun getTitleAndContent( + originalContent: CharSequence + ): Pair { return getTitleAndContent(originalContent, false) } @@ -364,22 +407,31 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { ): Pair { val gutenbergView = gutenbergView ?: return Pair("", "") - val result: Array?> = arrayOfNulls(1) + val result: Array?> = + arrayOfNulls(1) val latch = CountDownLatch(1) - gutenbergView.getTitleAndContent(originalContent, object : TitleAndContentCallback { - override fun onResult(title: CharSequence, content: CharSequence) { - result[0] = Pair(title, content) - latch.countDown() - } - }, completeComposition) + gutenbergView.getTitleAndContent( + originalContent, + object : TitleAndContentCallback { + override fun onResult( + title: CharSequence, content: CharSequence + ) { + result[0] = Pair(title, content) + latch.countDown() + } + }, + completeComposition + ) val finalResult = try { latch.await() result[0] } catch (e: InterruptedException) { AppLog.w( - AppLog.T.EDITOR, "Thread interrupted while waiting for title and content from Gutenberg editor: $e" + AppLog.T.EDITOR, + "Thread interrupted while waiting for title and " + + "content from Gutenberg editor: $e" ) Thread.currentThread().interrupt() null @@ -392,33 +444,36 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { return GUTENBERG_EDITOR_NAME } - /** - * Returns the contents of the content field from the JavaScript editor. Should be called from a background thread - * where possible. - */ @Throws(EditorFragmentNotAddedException::class) override fun getContent(originalContent: CharSequence?): CharSequence { if (!isAdded) { throw EditorFragmentNotAddedException() } - return "" + return getTitleAndContent(originalContent ?: "").second } override fun getTitleOrContentChanged(): LiveData { return textWatcher.afterTextChanged } - override fun appendMediaFiles(mediaList: MutableMap) { + override fun appendMediaFiles( + mediaList: MutableMap + ) { + // appendMediaFile may be called from a background thread + // (example: EditPostActivity.java#L2165) and Activity may + // have already be gone. + // Ticket: https://github.com/wordpress-mobile/WordPress-Android/issues/7386 if (activity == null) { - // appendMediaFile may be called from a background thread (example: EditPostActivity.java#L2165) and - // Activity may have already be gone. - // Ticket: https://github.com/wordpress-mobile/WordPress-Android/issues/7386 - AppLog.d(AppLog.T.MEDIA, "appendMediaFiles() called but Activity is null!") + AppLog.d( + AppLog.T.MEDIA, + "appendMediaFiles() called but Activity is null!" + ) return } - // Get media URL of first of media first to check if it is network or local one. + // Get media URL of first of media first to check + // if it is network or local one. var mediaUrl: String? = "" val mediaUrls: Array = mediaList.keys.toTypedArray() if (mediaUrls.isNotEmpty()) { @@ -432,52 +487,38 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { return } - val processedMediaList = mediaList.entries.map { (url, mediaFile) -> - val mediaId = mediaFile!!.mediaId.toInt() - Media.createMediaUsingMimeType( - mediaId, url!!, mediaFile.mimeType, mediaFile.caption, mediaFile.title, mediaFile.alt - ) - } + val processedMediaList = + mediaList.entries.map { (url, mediaFile) -> + val mediaId = mediaFile!!.mediaId.toInt() + Media.createMediaUsingMimeType( + mediaId, url!!, mediaFile.mimeType, + mediaFile.caption, mediaFile.title, mediaFile.alt + ) + } val mediaString = Gson().toJson(processedMediaList) gutenbergView?.setMediaUploadAttachment(mediaString) } override fun onDestroy() { - gutenbergView?.let { gutenbergView -> - gutenbergView.destroy() + gutenbergView?.let { historyChangeListener = null featuredImageChangeListener = null } - editorStarted = false - isEditorDidMount = false super.onDestroy() } - fun startWithEditorSettings(editorSettings: String) { - if (gutenbergView == null || editorStarted) { - return - } - - val config = buildEditorConfiguration(editorSettings) - editorStarted = true - gutenbergView?.start(config) - } - fun setXPostsEnabled(enabled: Boolean) { isXPostsEnabled = enabled } - fun setNetworkRequestListener(listener: GutenbergView.NetworkRequestListener) { + fun setNetworkRequestListener( + listener: GutenbergView.NetworkRequestListener + ) { networkRequestListener = listener gutenbergView?.setNetworkRequestListener(listener) } - private fun buildEditorConfiguration(editorSettings: String): EditorConfiguration { - val settingsMap = settings!! - return EditorConfigurationBuilder.build(settingsMap, editorSettings) - } - override fun onUndoPressed() { gutenbergView?.undo() } @@ -493,67 +534,21 @@ class GutenbergKitEditorFragment : GutenbergKitEditorFragmentBase() { companion object { private const val GUTENBERG_EDITOR_NAME = "gutenberg" private const val KEY_HTML_MODE_ENABLED = "KEY_HTML_MODE_ENABLED" - private const val KEY_EDITOR_STARTED = "KEY_EDITOR_STARTED" - private const val KEY_EDITOR_DID_MOUNT = "KEY_EDITOR_DID_MOUNT" - private const val ARG_IS_NEW_POST = "param_is_new_post" - private const val ARG_GUTENBERG_WEB_VIEW_AUTH_DATA = "param_gutenberg_web_view_auth_data" const val ARG_FEATURED_IMAGE_ID: String = "featured_image_id" - const val ARG_JETPACK_FEATURES_ENABLED: String = "jetpack_features_enabled" - const val ARG_GUTENBERG_KIT_SETTINGS: String = "gutenberg_kit_settings" + const val ARG_GUTENBERG_KIT_SETTINGS: String = + "gutenberg_kit_settings" private const val CAPTURE_PHOTO_PERMISSION_REQUEST_CODE = 101 private const val CAPTURE_VIDEO_PERMISSION_REQUEST_CODE = 102 - private var settings: Map? = null - fun newInstance( - context: Context, - isNewPost: Boolean, - webViewAuthorizationData: GutenbergWebViewAuthorizationData?, - jetpackFeaturesEnabled: Boolean, - settings: Map? + configuration: EditorConfiguration ): GutenbergKitEditorFragment { val fragment = GutenbergKitEditorFragment() val args = Bundle() - args.putBoolean(ARG_IS_NEW_POST, isNewPost) - args.putBoolean(ARG_JETPACK_FEATURES_ENABLED, jetpackFeaturesEnabled) - args.putSerializable(ARG_GUTENBERG_KIT_SETTINGS, settings as Serializable?) - fragment.setArguments(args) - val db = getDatabase(context) - GutenbergKitEditorFragment.settings = settings - db?.addParcel(ARG_GUTENBERG_WEB_VIEW_AUTH_DATA, webViewAuthorizationData) + args.putParcelable(ARG_GUTENBERG_KIT_SETTINGS, configuration) + fragment.arguments = args return fragment } - - /** - * Simplified factory method that uses GutenbergKitSettingsBuilder for configuration. - * This reduces the activity's responsibility for detailed fragment setup. - */ - fun newInstanceWithBuilder( - context: Context, - isNewPost: Boolean, - jetpackFeaturesEnabled: Boolean, - config: GutenbergKitSettingsBuilder.GutenbergKitConfig - ): GutenbergKitEditorFragment { - val authorizationData = GutenbergKitSettingsBuilder.buildAuthorizationData( - siteConfig = config.siteConfig, - appConfig = config.appConfig - ) - - val settings = GutenbergKitSettingsBuilder.buildSettings( - siteConfig = config.siteConfig, - postConfig = config.postConfig, - appConfig = config.appConfig, - featureConfig = config.featureConfig - ) - - return newInstance( - context, - isNewPost, - authorizationData, - jetpackFeaturesEnabled, - settings - ) - } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragmentBase.java b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragmentBase.java index 1aa193d65885..2f7520dbb76b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragmentBase.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/posts/editor/GutenbergKitEditorFragmentBase.java @@ -151,5 +151,17 @@ public interface EditorFragmentListener extends DialogVisibilityProvider { void onOpenMediaLibraryRequested(org.wordpress.gutenberg.GutenbergView.OpenMediaLibraryConfig config); void onModalDialogOpened(String dialogType); void onModalDialogClosed(String dialogType); + + /** + * Returns the persisted post title for content recovery after WebView refresh. + * @return The most recently persisted title from autosave. + */ + String getPersistedTitle(); + + /** + * Returns the persisted post content for content recovery after WebView refresh. + * @return The most recently persisted content from autosave. + */ + String getPersistedContent(); } } diff --git a/WordPress/src/main/res/layout/fragment_gutenberg_kit_editor.xml b/WordPress/src/main/res/layout/fragment_gutenberg_kit_editor.xml index d20f0298540a..60cb5387e1ff 100644 --- a/WordPress/src/main/res/layout/fragment_gutenberg_kit_editor.xml +++ b/WordPress/src/main/res/layout/fragment_gutenberg_kit_editor.xml @@ -8,11 +8,4 @@ android:id="@+id/gutenberg_view_container" android:layout_width="match_parent" android:layout_height="match_parent" /> - - diff --git a/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilderTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilderTest.kt index 3f63e25fcfa3..de5ab603c1e8 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilderTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/posts/GutenbergKitSettingsBuilderTest.kt @@ -7,6 +7,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.wordpress.android.fluxc.network.UserAgent +import org.wordpress.gutenberg.model.PostTypeDetails @RunWith(MockitoJUnitRunner::class) @Suppress("LargeClass") @@ -409,7 +410,8 @@ class GutenbergKitSettingsBuilderTest { remotePostId = 456L, isPage = false, title = "Test Post", - content = "Test Content" + content = "Test Content", + status = "publish" ) val settings = GutenbergKitSettingsBuilder.buildSettings( @@ -427,7 +429,7 @@ class GutenbergKitSettingsBuilderTest { // Verify all settings are correctly configured assertThat(settings["postId"]).isEqualTo(456) - assertThat(settings["postType"]).isEqualTo("post") + assertThat(settings["postType"]).isEqualTo(PostTypeDetails.post) assertThat(settings["postTitle"]).isEqualTo("Test Post") assertThat(settings["postContent"]).isEqualTo("Test Content") assertThat(settings["siteURL"]).isEqualTo("https://example.wordpress.com") @@ -461,7 +463,8 @@ class GutenbergKitSettingsBuilderTest { remotePostId = 100L, isPage = true, title = "Test Page", - content = "Page Content" + content = "Page Content", + status = "draft" ) val settings = GutenbergKitSettingsBuilder.buildSettings( @@ -474,7 +477,7 @@ class GutenbergKitSettingsBuilderTest { featureConfig = createFeatureConfig(isPluginsFeatureEnabled = true) ) - assertThat(settings["postType"]).isEqualTo("page") + assertThat(settings["postType"]).isEqualTo(PostTypeDetails.page) assertThat(settings["authHeader"] as String).startsWith("Basic ") assertThat(settings["siteApiRoot"]).isEqualTo("https://jetpack-site.com/wp-json/") assertThat(settings["siteApiNamespace"] as Array<*>).isEmpty() @@ -579,7 +582,8 @@ class GutenbergKitSettingsBuilderTest { remotePostId = null, isPage = false, title = null, - content = null + content = null, + status = null ) val settings = GutenbergKitSettingsBuilder.buildSettings( @@ -594,7 +598,28 @@ class GutenbergKitSettingsBuilderTest { assertThat(settings["postId"]).isNull() assertThat(settings["postTitle"]).isNull() assertThat(settings["postContent"]).isNull() - assertThat(settings["postType"]).isEqualTo("post") // Still defaults to post + assertThat(settings["status"]).isNull() + assertThat(settings["postType"]).isEqualTo(PostTypeDetails.post) // Still defaults to post + } + + @Test + fun `post status is included in settings`() { + val testCases = listOf("draft", "publish", "pending", "private", "future", "trash") + + testCases.forEach { status -> + val postConfig = createPostConfig(status = status) + + val settings = GutenbergKitSettingsBuilder.buildSettings( + siteConfig = createSiteConfig(), + postConfig = postConfig, + appConfig = createAppConfig(), + featureConfig = createFeatureConfig() + ) + + assertThat(settings["status"]) + .withFailMessage("Expected status=$status in settings") + .isEqualTo(status) + } } // ===== Helper Methods ===== @@ -651,11 +676,13 @@ class GutenbergKitSettingsBuilderTest { remotePostId: Long? = 1L, isPage: Boolean = false, title: String? = "Test", - content: String? = "Content" + content: String? = "Content", + status: String? = "draft" ) = GutenbergKitSettingsBuilder.PostConfig( remotePostId = remotePostId, isPage = isPage, title = title, - content = content + content = content, + status = status ) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c2f1be73103..1b7b42e87949 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,7 +73,7 @@ google-play-review = '2.0.2' google-services = '4.4.4' gravatar = '2.5.0' greenrobot-eventbus = '3.3.1' -gutenberg-kit = 'v0.11.1' +gutenberg-kit = 'v0.15.2' gutenberg-mobile = 'v1.121.0' indexos-media-for-mobile = '43a9026f0973a2f0a74fa813132f6a16f7499c3a' jackson-databind = '2.12.7.1'