diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt index 71f26c53..4f8a64ef 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/base/NavDestination.kt @@ -1,6 +1,7 @@ package dev.hotwire.turbo.demo.base import android.net.Uri +import android.view.MenuItem import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.SHARE_STATE_ON @@ -11,10 +12,12 @@ import dev.hotwire.turbo.config.context import dev.hotwire.turbo.demo.R import dev.hotwire.turbo.demo.util.BASE_URL import dev.hotwire.turbo.nav.TurboNavDestination -import dev.hotwire.turbo.nav.TurboNavPresentationContext -import dev.hotwire.turbo.nav.TurboNavPresentationContext.* +import dev.hotwire.turbo.nav.TurboNavPresentationContext.MODAL interface NavDestination : TurboNavDestination { + val menuProgress: MenuItem? + get() = toolbarForNavigation()?.menu?.findItem(R.id.menu_progress) + override fun shouldNavigateTo(newLocation: String): Boolean { return when (isNavigable(newLocation)) { true -> true diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebBottomSheetFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebBottomSheetFragment.kt index 7008a8b1..0985677c 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebBottomSheetFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebBottomSheetFragment.kt @@ -1,8 +1,28 @@ package dev.hotwire.turbo.demo.features.web +import android.os.Bundle +import android.view.View +import dev.hotwire.turbo.demo.R import dev.hotwire.turbo.demo.base.NavDestination import dev.hotwire.turbo.fragments.TurboWebBottomSheetDialogFragment import dev.hotwire.turbo.nav.TurboNavGraphDestination @TurboNavGraphDestination(uri = "turbo://fragment/web/modal/sheet") -class WebBottomSheetFragment : TurboWebBottomSheetDialogFragment(), NavDestination +class WebBottomSheetFragment : TurboWebBottomSheetDialogFragment(), NavDestination { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupMenu() + } + + override fun onFormSubmissionStarted(location: String) { + menuProgress?.isVisible = true + } + + override fun onFormSubmissionFinished(location: String) { + menuProgress?.isVisible = false + } + + private fun setupMenu() { + toolbarForNavigation()?.inflateMenu(R.menu.web) + } +} diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt index 704a4978..1637640d 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt @@ -1,19 +1,38 @@ package dev.hotwire.turbo.demo.features.web +import android.os.Bundle +import android.view.View +import dev.hotwire.turbo.demo.R import dev.hotwire.turbo.demo.base.NavDestination import dev.hotwire.turbo.demo.util.SIGN_IN_URL import dev.hotwire.turbo.fragments.TurboWebFragment import dev.hotwire.turbo.nav.TurboNavGraphDestination -import dev.hotwire.turbo.visit.TurboVisitAction import dev.hotwire.turbo.visit.TurboVisitAction.REPLACE import dev.hotwire.turbo.visit.TurboVisitOptions @TurboNavGraphDestination(uri = "turbo://fragment/web") open class WebFragment : TurboWebFragment(), NavDestination { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupMenu() + } + + override fun onFormSubmissionStarted(location: String) { + menuProgress?.isVisible = true + } + + override fun onFormSubmissionFinished(location: String) { + menuProgress?.isVisible = false + } + override fun onVisitErrorReceived(location: String, errorCode: Int) { when (errorCode) { 401 -> navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE)) else -> super.onVisitErrorReceived(location, errorCode) } } + + private fun setupMenu() { + toolbarForNavigation()?.inflateMenu(R.menu.web) + } } diff --git a/demo/src/main/res/layout/toolbar_progress.xml b/demo/src/main/res/layout/toolbar_progress.xml new file mode 100644 index 00000000..7d14dcb3 --- /dev/null +++ b/demo/src/main/res/layout/toolbar_progress.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/demo/src/main/res/menu/web.xml b/demo/src/main/res/menu/web.xml new file mode 100644 index 00000000..a96f5cda --- /dev/null +++ b/demo/src/main/res/menu/web.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml index cd13d4cd..0ddabcc8 100644 --- a/demo/src/main/res/values/strings.xml +++ b/demo/src/main/res/values/strings.xml @@ -2,4 +2,5 @@ Turbo Native Error loading page The demo server may be starting up. Pull-to-refresh to try again in a minute. + Loading… diff --git a/turbo/src/main/assets/js/turbo_bridge.js b/turbo/src/main/assets/js/turbo_bridge.js index 1cdd237e..55af01f4 100644 --- a/turbo/src/main/assets/js/turbo_bridge.js +++ b/turbo/src/main/assets/js/turbo_bridge.js @@ -144,6 +144,14 @@ }) } + formSubmissionStarted(formSubmission) { + TurboSession.formSubmissionStarted(formSubmission.location.toString()) + } + + formSubmissionFinished(formSubmission) { + TurboSession.formSubmissionFinished(formSubmission.location.toString()) + } + pageInvalidated() { TurboSession.pageInvalidated() } diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationRepository.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationRepository.kt index fcb91126..fc8c4b5d 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationRepository.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/config/TurboPathConfigurationRepository.kt @@ -6,7 +6,6 @@ import androidx.core.content.edit import dev.hotwire.turbo.http.TurboHttpClient import dev.hotwire.turbo.util.dispatcherProvider import dev.hotwire.turbo.util.toJson -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Request import java.io.IOException diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt index 70bf3841..9483558e 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt @@ -231,6 +231,14 @@ internal class TurboWebFragmentDelegate( return navDestination } + override fun formSubmissionStarted(location: String) { + callback.onFormSubmissionStarted(location) + } + + override fun formSubmissionFinished(location: String) { + callback.onFormSubmissionFinished(location) + } + // ----------------------------------------------------------------------- // Private // ----------------------------------------------------------------------- diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt index 0420018c..7ac6d92a 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt @@ -66,6 +66,16 @@ interface TurboWebFragmentCallback { */ fun onVisitErrorReceived(location: String, errorCode: Int) {} + /** + * Called when a Turbo form submission has started. + */ + fun onFormSubmissionStarted(location: String) {} + + /** + * Called when a Turbo form submission has finished. + */ + fun onFormSubmissionFinished(location: String) {} + /** * Called when the Turbo visit resulted in an error, but a cached * snapshot is being displayed, which may be stale. diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt index 48e07e81..b1953267 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/nav/TurboNavRule.kt @@ -9,7 +9,6 @@ import dev.hotwire.turbo.config.* import dev.hotwire.turbo.session.TurboSessionModalResult import dev.hotwire.turbo.visit.TurboVisitAction import dev.hotwire.turbo.visit.TurboVisitOptions -import java.net.URI @Suppress("MemberVisibilityCanBePrivate") internal class TurboNavRule( diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/observers/TurboWindowThemeObserver.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/observers/TurboWindowThemeObserver.kt index 0ee06abb..999f5e08 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/observers/TurboWindowThemeObserver.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/observers/TurboWindowThemeObserver.kt @@ -5,7 +5,6 @@ import android.os.Build import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR import android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR import android.view.Window -import android.view.WindowInsetsController import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import androidx.annotation.RequiresApi import androidx.lifecycle.Lifecycle diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt index df2504af..7707d20c 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt @@ -354,6 +354,46 @@ class TurboSession internal constructor( } } + /** + * Called by Turbo bridge when a form submission has started. + * + * Warning: This method is public so it can be used as a Javascript Interface. + * You should never call this directly as it could lead to unintended behavior. + * + * @param location The location of the form submission. + */ + @JavascriptInterface + fun formSubmissionStarted(location: String) { + logEvent( + "formSubmissionStarted", + "location" to location + ) + + currentVisit?.let { + callback { it.formSubmissionStarted(location) } + } + } + + /** + * Called by Turbo bridge when a form submission has finished. + * + * Warning: This method is public so it can be used as a Javascript Interface. + * You should never call this directly as it could lead to unintended behavior. + * + * @param location The location of the form submission. + */ + @JavascriptInterface + fun formSubmissionFinished(location: String) { + logEvent( + "formSubmissionFinished", + "location" to location + ) + + currentVisit?.let { + callback { it.formSubmissionFinished(location) } + } + } + /** * Called when Turbo bridge detects that the page being visited has been invalidated, * typically by new resources in the the page HEAD. diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt index b4f40c98..c3940d81 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt @@ -19,4 +19,6 @@ internal interface TurboSessionCallback { fun visitLocationStarted(location: String) fun visitProposedToLocation(location: String, options: TurboVisitOptions) fun visitNavDestination(): TurboNavDestination + fun formSubmissionStarted(location: String) + fun formSubmissionFinished(location: String) } diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt index 1620f504..1e756074 100644 --- a/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt @@ -115,6 +115,22 @@ class TurboSessionTest { assertThat(session.restorationIdentifiers.size()).isEqualTo(1) } + @Test + fun visitFormSubmissionStartedFiresCallback() { + session.currentVisit = visit + session.formSubmissionStarted(visit.location) + + verify(callback).formSubmissionStarted(visit.location) + } + + @Test + fun visitFormSubmissionFinishedFiresCallback() { + session.currentVisit = visit + session.formSubmissionFinished(visit.location) + + verify(callback).formSubmissionFinished(visit.location) + } + @Test fun pageLoadedSavesRestorationIdentifier() { val restorationIdentifier = "67890"