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 050a347fe988..8834175d93c9 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 @@ -47,6 +47,7 @@ import com.automattic.android.tracks.crashlogging.JsExceptionStackTraceElement import com.google.android.material.appbar.AppBarLayout import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import com.google.gson.JsonObject import kotlinx.parcelize.parcelableCreator import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -82,11 +83,13 @@ import org.wordpress.android.editor.savedinstance.SavedInstanceDatabase.Companio import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.action.AccountAction import org.wordpress.android.fluxc.generated.AccountActionBuilder +import org.wordpress.android.fluxc.generated.EditorSettingsActionBuilder import org.wordpress.android.fluxc.generated.EditorThemeActionBuilder import org.wordpress.android.fluxc.generated.PostActionBuilder import org.wordpress.android.fluxc.generated.SiteActionBuilder import org.wordpress.android.fluxc.model.AccountModel import org.wordpress.android.fluxc.model.CauseOfOnPostChanged +import org.wordpress.android.fluxc.model.EditorSettings import org.wordpress.android.fluxc.model.EditorTheme import org.wordpress.android.fluxc.model.EditorThemeSupport import org.wordpress.android.fluxc.model.MediaModel @@ -99,6 +102,9 @@ import org.wordpress.android.fluxc.network.UserAgent import org.wordpress.android.fluxc.network.rest.wpcom.site.PrivateAtomicCookie import org.wordpress.android.fluxc.store.AccountStore import org.wordpress.android.fluxc.store.AccountStore.OnAccountChanged +import org.wordpress.android.fluxc.store.EditorSettingsStore +import org.wordpress.android.fluxc.store.EditorSettingsStore.FetchEditorSettingsPayload +import org.wordpress.android.fluxc.store.EditorSettingsStore.OnEditorSettingsChanged import org.wordpress.android.fluxc.store.EditorThemeStore import org.wordpress.android.fluxc.store.EditorThemeStore.FetchEditorThemePayload import org.wordpress.android.fluxc.store.EditorThemeStore.OnEditorThemeChanged @@ -348,6 +354,8 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor @Inject lateinit var editorThemeStore: EditorThemeStore + @Inject lateinit var editorSettingsStore: EditorSettingsStore + @Inject lateinit var imageLoader: FluxCImageLoader @Inject lateinit var shortcutUtils: ShortcutUtils @@ -3636,9 +3644,12 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor } private fun onEditorFinalTouchesBeforeShowing() { - refreshEditorContent() + if (editorFragment !is GutenbergKitEditorFragment) { + refreshEditorContent() + } onEditorFinalTouchesBeforeShowingForGutenbergIfNeeded() + onEditorFinalTouchesBeforeShowingForGutenbergKitIfNeeded() onEditorFinalTouchesBeforeShowingForAztecIfNeeded() } private fun onEditorFinalTouchesBeforeShowingForGutenbergIfNeeded() { @@ -3661,6 +3672,13 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor (editorFragment as GutenbergEditorFragment).resetUploadingMediaToFailed(mediaIds) } } + + private fun onEditorFinalTouchesBeforeShowingForGutenbergKitIfNeeded() { + if (showGutenbergEditor && editorFragment is GutenbergKitEditorFragment) { + refreshEditorSettings() + } + } + private fun onEditorFinalTouchesBeforeShowingForAztecIfNeeded() { if (showAztecEditor && editorFragment is AztecEditorFragment) { val entryPoint = @@ -4065,6 +4083,18 @@ class EditPostActivity : BaseAppCompatActivity(), EditorFragmentActivity, Editor postEditorAnalyticsSession?.editorSettingsFetched(editorThemeSupport.isBlockBasedTheme, event.endpoint.value) } + private fun refreshEditorSettings() { + val payload = FetchEditorSettingsPayload(siteModel) + dispatcher.dispatch(EditorSettingsActionBuilder.newFetchEditorSettingsAction(payload)) + } + + @Suppress("unused") + @Subscribe(threadMode = ThreadMode.MAIN_ORDERED) + fun onEditorSettingsChanged(event: OnEditorSettingsChanged) { + val editorSettings = event.editorSettings ?: EditorSettings(JsonObject()) + (editorFragment as? GutenbergKitEditorFragment)?.startWithEditorSettings(editorSettings.toJsonString()) + } + // EditPostActivityHook methods override fun getEditPostRepository() = editPostRepository override fun getSite() = siteModel diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dbe830e2a172..262d2b05c765 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -72,7 +72,7 @@ google-play-services-auth = '20.4.1' google-services = '4.4.2' gravatar = '2.4.1' greenrobot-eventbus = '3.3.1' -gutenberg-kit = 'trunk-a03e0dae10a404c88c215bfcee3176df951302f5' +gutenberg-kit = 'trunk-fa72e630203e7472d55f4abedfd5c462d2333584' gutenberg-mobile = 'v1.121.0' indexos-media-for-mobile = '43a9026f0973a2f0a74fa813132f6a16f7499c3a' jackson-databind = '2.12.7.1' diff --git a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergKitEditorFragment.java b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergKitEditorFragment.java index 8f6ea997a8d7..7046a2343ee6 100644 --- a/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergKitEditorFragment.java +++ b/libs/editor/src/main/java/org/wordpress/android/editor/gutenberg/GutenbergKitEditorFragment.java @@ -89,14 +89,16 @@ public class GutenbergKitEditorFragment extends EditorFragmentAbstract implement @Nullable private LogJsExceptionListener mOnLogJsExceptionListener = null; private boolean mEditorDidMount; + @Nullable + private View mRootView; @Nullable private static Map mSettings; public static GutenbergKitEditorFragment newInstance(Context context, - boolean isNewPost, - GutenbergWebViewAuthorizationData webViewAuthorizationData, - boolean jetpackFeaturesEnabled, - @Nullable Map settings) { + boolean isNewPost, + @Nullable GutenbergWebViewAuthorizationData webViewAuthorizationData, + boolean jetpackFeaturesEnabled, + @Nullable Map settings) { GutenbergKitEditorFragment fragment = new GutenbergKitEditorFragment(); Bundle args = new Bundle(); args.putBoolean(ARG_IS_NEW_POST, isNewPost); @@ -137,11 +139,20 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa ((EditorFragmentActivity) getActivity()).initializeEditorFragment(); } + mEditorFragmentListener.onEditorFragmentInitialized(); + + mRootView = inflater.inflate(R.layout.fragment_gutenberg_kit_editor, container, false); + ViewGroup gutenbergViewContainer = mRootView.findViewById(R.id.gutenberg_view_container); + mGutenbergView = GutenbergWebViewPool.getPreloadedWebView(requireContext()); mGutenbergView.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT )); + gutenbergViewContainer.addView(mGutenbergView); + + setEditorProgressBarVisibility(true); + mGutenbergView.setOnFileChooserRequestedListener((intent, requestCode) -> { startActivityForResult(intent, requestCode); return null; @@ -151,31 +162,12 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa mGutenbergView.setOpenMediaLibraryListener(mOpenMediaLibraryListener); mGutenbergView.setLogJsExceptionListener(mOnLogJsExceptionListener); mGutenbergView.setEditorDidBecomeAvailable(view -> { + mEditorDidMount = true; mEditorFragmentListener.onEditorFragmentContentReady(new ArrayList<>(), false); + setEditorProgressBarVisibility(false); }); - Integer postId = (Integer) mSettings.get("postId"); - if (postId != null && postId == 0) { - postId = -1; - } - - EditorConfiguration config = new EditorConfiguration.Builder() - .setTitle((String) mSettings.get("postTitle")) - .setContent((String) mSettings.get("postContent")) - .setPostId(postId) - .setPostType((String) mSettings.get("postType")) - .setThemeStyles((Boolean) mSettings.get("themeStyles")) - .setPlugins((Boolean) mSettings.get("plugins")) - .setSiteApiRoot((String) mSettings.get("siteApiRoot")) - .setSiteApiNamespace((String[]) mSettings.get("siteApiNamespace")) - .setNamespaceExcludedPaths((String[]) mSettings.get("namespaceExcludedPaths")) - .setAuthHeader((String) mSettings.get("authHeader")) - .setWebViewGlobals((List) mSettings.get("webViewGlobals")) - .build(); - - mGutenbergView.start(config); - - return mGutenbergView; + return mRootView; } @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { @@ -218,6 +210,13 @@ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent d @Override public void onResume() { super.onResume(); + setEditorProgressBarVisibility(!mEditorDidMount); + } + + private void setEditorProgressBarVisibility(boolean shown) { + if (isAdded() && mRootView != null) { + mRootView.findViewById(R.id.editor_progress).setVisibility(shown ? View.VISIBLE : View.GONE); + } } @Override @@ -543,6 +542,34 @@ public void onEditorThemeUpdated(Bundle editorTheme) { // Unused, no-op retained for the shared interface with Gutenberg } + public void startWithEditorSettings(@NonNull String editorSettings) { + if (mGutenbergView == null) { + return; + } + + Integer postId = (Integer) mSettings.get("postId"); + if (postId != null && postId == 0) { + postId = -1; + } + + EditorConfiguration config = new EditorConfiguration.Builder() + .setTitle((String) mSettings.get("postTitle")) + .setContent((String) mSettings.get("postContent")) + .setPostId(postId) + .setPostType((String) mSettings.get("postType")) + .setThemeStyles((Boolean) mSettings.get("themeStyles")) + .setPlugins((Boolean) mSettings.get("plugins")) + .setSiteApiRoot((String) mSettings.get("siteApiRoot")) + .setSiteApiNamespace((String[]) mSettings.get("siteApiNamespace")) + .setNamespaceExcludedPaths((String[]) mSettings.get("namespaceExcludedPaths")) + .setAuthHeader((String) mSettings.get("authHeader")) + .setWebViewGlobals((List) mSettings.get("webViewGlobals")) + .setEditorSettings(editorSettings) + .build(); + + mGutenbergView.start(config); + } + @Override public void showNotice(String message) { // Unused, no-op retained for the shared interface with Gutenberg diff --git a/libs/editor/src/main/res/layout/fragment_gutenberg_kit_editor.xml b/libs/editor/src/main/res/layout/fragment_gutenberg_kit_editor.xml new file mode 100644 index 000000000000..d20f0298540a --- /dev/null +++ b/libs/editor/src/main/res/layout/fragment_gutenberg_kit_editor.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/action/EditorSettingsAction.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/action/EditorSettingsAction.kt new file mode 100644 index 000000000000..c857d38d6a1f --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/action/EditorSettingsAction.kt @@ -0,0 +1,12 @@ +package org.wordpress.android.fluxc.action + +import org.wordpress.android.fluxc.annotations.Action +import org.wordpress.android.fluxc.annotations.ActionEnum +import org.wordpress.android.fluxc.annotations.action.IAction +import org.wordpress.android.fluxc.store.EditorSettingsStore.FetchEditorSettingsPayload + +@ActionEnum +enum class EditorSettingsAction : IAction { + @Action(payloadType = FetchEditorSettingsPayload::class) + FETCH_EDITOR_SETTINGS +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/action/EditorSettingsActionBuilder.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/action/EditorSettingsActionBuilder.kt new file mode 100644 index 000000000000..c3b4b309e0a5 --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/action/EditorSettingsActionBuilder.kt @@ -0,0 +1,10 @@ +package org.wordpress.android.fluxc.action + +import org.wordpress.android.fluxc.annotations.action.Action +import org.wordpress.android.fluxc.store.EditorSettingsStore.FetchEditorSettingsPayload + +object EditorSettingsActionBuilder { + fun newFetchEditorSettingsAction(payload: FetchEditorSettingsPayload): Action { + return Action(EditorSettingsAction.FETCH_EDITOR_SETTINGS, payload) + } +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/EditorSettings.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/EditorSettings.kt new file mode 100644 index 000000000000..e7818dcdaafc --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/EditorSettings.kt @@ -0,0 +1,17 @@ +package org.wordpress.android.fluxc.model + +import com.google.gson.JsonObject +import org.wordpress.android.fluxc.persistence.EditorSettingsSqlUtils.EditorSettingsBuilder + +class EditorSettings(val rawSettings: JsonObject) { + fun toJsonString(): String { + return rawSettings.toString() + } + + fun toBuilder(site: SiteModel): EditorSettingsBuilder { + return EditorSettingsBuilder().apply { + localSiteId = site.id + rawSettings = this@EditorSettings.rawSettings.toString() + } + } +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/EditorSettingsSqlUtils.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/EditorSettingsSqlUtils.kt new file mode 100644 index 000000000000..bbfa54580f09 --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/EditorSettingsSqlUtils.kt @@ -0,0 +1,66 @@ +package org.wordpress.android.fluxc.persistence + +import com.google.gson.JsonParser +import com.wellsql.generated.EditorSettingsTable +import com.yarolegovich.wellsql.WellSql +import com.yarolegovich.wellsql.core.Identifiable +import com.yarolegovich.wellsql.core.annotation.Column +import com.yarolegovich.wellsql.core.annotation.PrimaryKey +import com.yarolegovich.wellsql.core.annotation.Table +import org.wordpress.android.fluxc.model.EditorSettings +import org.wordpress.android.fluxc.model.SiteModel + +class EditorSettingsSqlUtils { + fun replaceEditorSettingsForSite(site: SiteModel, editorSettings: EditorSettings?) { + deleteEditorSettingsForSite(site) + if (editorSettings == null) return + makeEditorSettings(site, editorSettings) + } + + fun getEditorSettingsForSite(site: SiteModel): EditorSettings? { + return WellSql.select(EditorSettingsBuilder::class.java) + .limit(1) + .where() + .equals(EditorSettingsTable.LOCAL_SITE_ID, site.id) + .endWhere() + .asModel + .firstOrNull() + ?.toEditorSettings() + } + + fun deleteEditorSettingsForSite(site: SiteModel) { + WellSql.delete(EditorSettingsBuilder::class.java) + .where() + .equals(EditorSettingsTable.LOCAL_SITE_ID, site.id) + .endWhere() + .execute() + } + + private fun makeEditorSettings(site: SiteModel, editorSettings: EditorSettings) { + val builder = editorSettings.toBuilder(site) + WellSql.insert(builder).execute() + } + + @Table(name = "EditorSettings") + data class EditorSettingsBuilder(@PrimaryKey @Column private var mId: Int = -1) : Identifiable { + @Column var localSiteId: Int = -1 + @JvmName("getLocalSiteId") + get + @JvmName("setLocalSiteId") + set + @Column var rawSettings: String? = null + + override fun setId(id: Int) { + this.mId = id + } + + override fun getId() = mId + + fun toEditorSettings(): EditorSettings? { + return rawSettings?.let { + val jsonObject = JsonParser.parseString(it).asJsonObject + EditorSettings(jsonObject) + } + } + } +} diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt index 02fd550c8b76..96459c416134 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt @@ -41,7 +41,7 @@ open class WellSqlConfig : DefaultWellConfig { annotation class AddOn override fun getDbVersion(): Int { - return 205 + return 206 } override fun getDbName(): String { @@ -2055,6 +2055,17 @@ open class WellSqlConfig : DefaultWellConfig { } 204 -> db.execSQL("ALTER TABLE SiteModel ADD IS_DELETED INTEGER DEFAULT 0") + + 205 -> migrate(version) { + db.execSQL(""" + CREATE TABLE IF NOT EXISTS EditorSettings ( + _id INTEGER PRIMARY KEY AUTOINCREMENT, + LOCAL_SITE_ID INTEGER NOT NULL, + RAW_SETTINGS TEXT, + FOREIGN KEY (LOCAL_SITE_ID) REFERENCES SiteModel(_id) ON DELETE CASCADE + ) + """.trimIndent()) + } } } db.setTransactionSuccessful() diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/EditorSettingsStore.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/EditorSettingsStore.kt new file mode 100644 index 000000000000..da85e536edca --- /dev/null +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/EditorSettingsStore.kt @@ -0,0 +1,112 @@ +package org.wordpress.android.fluxc.store + +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import org.wordpress.android.fluxc.Dispatcher +import org.wordpress.android.fluxc.Payload +import org.wordpress.android.fluxc.action.EditorSettingsAction +import org.wordpress.android.fluxc.action.EditorSettingsAction.FETCH_EDITOR_SETTINGS +import org.wordpress.android.fluxc.annotations.action.Action +import org.wordpress.android.fluxc.model.EditorSettings +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.network.BaseRequest.BaseNetworkError +import org.wordpress.android.fluxc.persistence.EditorSettingsSqlUtils +import org.wordpress.android.fluxc.store.ReactNativeFetchResponse.Error +import org.wordpress.android.fluxc.store.ReactNativeFetchResponse.Success +import org.wordpress.android.fluxc.tools.CoroutineEngine +import org.wordpress.android.util.AppLog +import javax.inject.Inject +import javax.inject.Singleton + +private const val EDITOR_SETTINGS_REQUEST_PATH = "wp-block-editor/v1/settings" + +@Singleton +class EditorSettingsStore @Inject constructor( + private val reactNativeStore: ReactNativeStore, + private val coroutineEngine: CoroutineEngine, + dispatcher: Dispatcher +) : Store(dispatcher) { + private val editorSettingsSqlUtils = EditorSettingsSqlUtils() + + class FetchEditorSettingsPayload(val site: SiteModel) : Payload() + + data class OnEditorSettingsChanged( + val editorSettings: EditorSettings?, + val siteId: Int, + val causeOfChange: EditorSettingsAction, + val isFromCache: Boolean = false + ) : Store.OnChanged() { + constructor(error: EditorSettingsError, causeOfChange: EditorSettingsAction) : + this(editorSettings = null, siteId = -1, causeOfChange = causeOfChange) { + this.error = error + } + } + + class EditorSettingsError(var message: String? = null) : OnChangedError + + @Subscribe(threadMode = ThreadMode.ASYNC) + override fun onAction(action: Action<*>) { + val actionType = action.type as? EditorSettingsAction ?: return + when (actionType) { + FETCH_EDITOR_SETTINGS -> { + coroutineEngine.launch( + AppLog.T.API, + this, + EditorSettingsStore::class.java.simpleName + ": On FETCH_EDITOR_SETTINGS" + ) { + val payload = action.payload as FetchEditorSettingsPayload + fetchEditorSettings(payload.site, actionType) + } + } + } + } + + override fun onRegister() { + AppLog.d(AppLog.T.API, EditorSettingsStore::class.java.simpleName + " onRegister") + } + + private suspend fun fetchEditorSettings(site: SiteModel, action: EditorSettingsAction) { + // First emit cached data if available + val cachedSettings = editorSettingsSqlUtils.getEditorSettingsForSite(site) + if (cachedSettings != null) { + emitChange(OnEditorSettingsChanged(cachedSettings, site.id, action, isFromCache = true)) + } + + // Then fetch fresh data + val response = reactNativeStore.executeGetRequest(site, EDITOR_SETTINGS_REQUEST_PATH, false) + + when (response) { + is Success -> { + if (response.result == null || !response.result.isJsonObject) { + emitChange(OnEditorSettingsChanged( + EditorSettingsError("Response does not contain editor settings"), + action + )) + return + } + + val editorSettings = EditorSettings(response.result.asJsonObject) + // Update cache + editorSettingsSqlUtils.replaceEditorSettingsForSite(site, editorSettings) + + // Only emit change if the data is different from cache + if (cachedSettings != editorSettings) { + val onChanged = OnEditorSettingsChanged(editorSettings, site.id, action) + emitChange(onChanged) + } + } + is Error -> { + if (cachedSettings != null) { + val onChanged = OnEditorSettingsChanged(cachedSettings, site.id, action, isFromCache = true) + emitChange(onChanged) + } else { + val onChanged = OnEditorSettingsChanged( + EditorSettingsError(response.error.message), + action + ) + emitChange(onChanged) + } + } + } + } +}