Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce scrollEnabled property, refactor a way of setting the WebView settings, and apply minor fixes to the native codebase #190

Merged
merged 15 commits into from
Apr 24, 2024
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
4 changes: 4 additions & 0 deletions packages/turbo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ The name of the application as used in the user agent string. Please note that c

Enables pull to refresh functionality. Default value is `true`.

### `scrollEnabled`

Enables scrolling in the webview. Default value is `true`.

### `stradaComponents`

`VisitableView` supports defining [Strada components](https://strada.hotwired.dev/) that receive and reply to messages from web components that are present on the page within one session. This prop accepts an array of Strada components that will be registered in the webview.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStateAtLeast
import com.facebook.react.BuildConfig
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import dev.hotwire.turbo.errors.TurboVisitError
import dev.hotwire.turbo.session.TurboSession
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.reactnativeturbowebview

import android.content.Context
import android.graphics.Bitmap
import android.view.MotionEvent
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.WebSettings
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
Expand All @@ -16,12 +18,7 @@ import dev.hotwire.turbo.views.TurboView
import dev.hotwire.turbo.views.TurboWebView
import dev.hotwire.turbo.visit.TurboVisitOptions
import dev.hotwire.turbo.R
import dev.hotwire.turbo.errors.HttpError
import dev.hotwire.turbo.errors.LoadError
import dev.hotwire.turbo.errors.TurboVisitError
import dev.hotwire.turbo.errors.WebError
import dev.hotwire.turbo.errors.WebSslError
import dev.hotwire.turbo.visit.TurboVisitAction

const val REFRESH_SCRIPT = "typeof Turbo.session.refresh === 'function'" +
"? Turbo.session.refresh(document.baseURI)" + // Turbo 8+
Expand All @@ -46,21 +43,34 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib

lateinit var sessionHandle: String
var applicationNameForUserAgent: String? = null
var scrollEnabled: Boolean = true
set(value) {
field = value
updateWebViewConfiguration()
}
var pullToRefreshEnabled: Boolean = true
set(value) {
field = value
setPullToRefresh(value)
}

// Session
private val session: RNSession by lazy {
RNSessionManager.findOrCreateSession(
reactContext,
sessionHandle,
applicationNameForUserAgent
)
}
private val webView: TurboWebView get() = session.webView
private var _session: RNSession? = null
private val session: RNSession?
get() {
if (_session != null) {
return _session
}

if (!::sessionHandle.isInitialized) {
return null
}

_session = RNSessionManager.findOrCreateSession(reactContext, sessionHandle, applicationNameForUserAgent)
return _session
}

private val webView: TurboWebView? get() = session?.webView

private var onConfirmHandler: ((result: Boolean) -> Unit)? = null
private var onAlertHandler: (() -> Unit)? = null
Expand Down Expand Up @@ -92,8 +102,22 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib
}
}

private fun updateWebViewConfiguration() {
if (webView == null) return

setOnTouchListener(webView!!, scrollEnabled)
}

private fun setOnTouchListener(webView: TurboWebView, scrollEnabled: Boolean) {
if (!scrollEnabled) {
webView.setOnTouchListener(OnTouchListener { _, event -> event.action == MotionEvent.ACTION_MOVE })
} else {
webView.setOnTouchListener(null)
}
}

private fun performVisit(restoreWithCachedSnapshot: Boolean, reload: Boolean) {
session.visit(
session?.visit(
url = url,
restoreWithCachedSnapshot = restoreWithCachedSnapshot,
reload = reload,
Expand All @@ -109,7 +133,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib
// Visit every time the WebView is reattached to the current Fragment.
if (isWebViewAttachedToNewDestination) {
val currentSessionVisitRestored =
!isInitialVisit && session.currentVisit?.destinationIdentifier == url.hashCode() && session.restoreCurrentVisit()
!isInitialVisit && session?.currentVisit?.destinationIdentifier == url.hashCode() && session?.restoreCurrentVisit() == true

if (!currentSessionVisitRestored) {
showProgressView()
Expand All @@ -121,11 +145,11 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib
}

override fun refresh() {
webView.evaluateJavascript(REFRESH_SCRIPT, null)
webView?.evaluateJavascript(REFRESH_SCRIPT, null)
}

override fun reload(displayProgress: Boolean) {
if (webView.url == null) return
if (webView?.url == null) return

turboView.webViewRefresh?.apply {
if (displayProgress && !isRefreshing) {
Expand Down Expand Up @@ -164,7 +188,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib

override fun onAttachedToWindow() {
super.onAttachedToWindow()
session.registerVisitableView(this)
session?.registerVisitableView(this)
visit()
}

Expand All @@ -173,24 +197,24 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib
// This can happen when the user uses one session for different
// bottom tabs. In this case, we need to remove the webview from
// the parent before attaching it to the new one.
if (webView.parent != null) {
(webView.parent as ViewGroup).removeView(webView)
if (webView!!.parent != null) {
(webView!!.parent as ViewGroup).removeView(webView)
}

// Re-layout the TurboView before attaching to make page restorations work correctly.
requestLayout()

turboView.attachWebView(webView) { attachedToNewDestination ->
turboView.attachWebView(webView!!) { attachedToNewDestination ->
onReady(attachedToNewDestination)
}
}

override fun detachWebView() {
screenshotView()

(webView.parent as ViewGroup?)?.endViewTransition(webView)
(webView!!.parent as ViewGroup?)?.endViewTransition(webView)

turboView.detachWebView(webView) {
turboView.detachWebView(webView!!) {
// Force layout to fix improper layout of the TurboWebView.
forceLayout()
}
Expand Down Expand Up @@ -261,7 +285,7 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib
// region SessionSubscriber

override fun injectJavaScript(script: String) {
webView.evaluateJavascript(script, null)
webView?.evaluateJavascript(script, null)
}

override fun handleMessage(message: WritableMap) {
Expand Down Expand Up @@ -311,16 +335,17 @@ class RNVisitableView(context: Context) : LinearLayout(context), SessionSubscrib

override fun visitRendered() {
sendEvent(RNVisitableViewEvent.LOAD, Arguments.createMap().apply {
putString("title", webView.title)
putString("url", webView.url)
putString("title", webView!!.title)
putString("url", webView!!.url)
})
updateWebViewConfiguration()
removeTransitionalViews()
}

override fun visitCompleted(completedOffline: Boolean) {
sendEvent(RNVisitableViewEvent.LOAD, Arguments.createMap().apply {
putString("title", webView.title)
putString("url", webView.url)
putString("title", webView!!.title)
putString("url", webView!!.url)
})
CookieManager
.getInstance()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class RNVisitableViewManager(
view.pullToRefreshEnabled = pullToRefreshEnabled
}

@ReactProp(name = "scrollEnabled")
fun setScrollEnabled(view: RNVisitableView, scrollEnabled: Boolean) {
view.scrollEnabled = scrollEnabled
}

override fun getCommandsMap(): MutableMap<String, Int> = RNVisitableViewCommand.values()
.associate {
Expand Down
67 changes: 45 additions & 22 deletions packages/turbo/ios/RNVisitableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,28 @@ let REFRESH_SCRIPT = "typeof Turbo.session.refresh === 'function'" +
class RNVisitableView: UIView, RNSessionSubscriber {
var id: UUID = UUID()
@objc var sessionHandle: NSString? = nil
@objc var applicationNameForUserAgent: NSString? = nil
@objc var url: NSString = "" {
didSet {
if(url != oldValue) {
visit()
}
}
}
@objc var applicationNameForUserAgent: NSString? = nil {
didSet {
webViewConfiguration.applicationNameForUserAgent = applicationNameForUserAgent as? String
}
}
@objc var pullToRefreshEnabled: Bool = true {
didSet {
controller!.visitableView.allowsPullToRefresh = pullToRefreshEnabled
}
}
@objc var scrollEnabled: Bool = true {
didSet {
configureWebView()
}
}
@objc var onMessage: RCTDirectEventBlock?
@objc var onVisitProposal: RCTDirectEventBlock?
@objc var onOpenExternalUrl: RCTDirectEventBlock?
Expand All @@ -43,23 +52,35 @@ class RNVisitableView: UIView, RNSessionSubscriber {
private var onConfirmHandler: ((Bool) -> Void)?
private var onAlertHandler: (() -> Void)?

private lazy var session: RNSession = RNSessionManager.shared.findOrCreateSession(sessionHandle: sessionHandle!, webViewConfiguration: webViewConfiguration)
private lazy var webView: WKWebView = session.webView
private lazy var webViewConfiguration: WKWebViewConfiguration = {
let configuration = WKWebViewConfiguration()
configuration.applicationNameForUserAgent = applicationNameForUserAgent as String?
return configuration
}()
private var _session: RNSession? = nil
private var session: RNSession? {
if (_session != nil) {
return _session
}

if (sessionHandle == nil) {
return nil
}

_session = RNSessionManager.shared.findOrCreateSession(sessionHandle: sessionHandle!, webViewConfiguration: webViewConfiguration)
return _session
}
private var webView: WKWebView? { session?.webView }
private var webViewConfiguration: WKWebViewConfiguration = WKWebViewConfiguration()

lazy var controller: RNVisitableViewController? = RNVisitableViewController(reactViewController: reactViewController(), delegate: self)

private var isRefreshing: Bool {
controller!.visitableView.isRefreshing
}

// var isModal: Bool {
// return controller.reactViewController()?.isModal()
// }

private func configureWebView() {
if (webView == nil) {
return
}

webView!.scrollView.isScrollEnabled = scrollEnabled
}

override func willMove(toWindow newWindow: UIWindow?) {
super.willMove(toWindow: newWindow)
Expand Down Expand Up @@ -95,6 +116,7 @@ class RNVisitableView: UIView, RNSessionSubscriber {

override func removeFromSuperview() {
super.removeFromSuperview()
_session = nil
controller = nil
}

Expand All @@ -105,7 +127,7 @@ class RNVisitableView: UIView, RNSessionSubscriber {
}

public func injectJavaScript(code: NSString) -> Void {
webView.evaluateJavaScript(code as String)
webView?.evaluateJavaScript(code as String)
}

public func sendAlertResult() -> Void {
Expand All @@ -120,23 +142,23 @@ class RNVisitableView: UIView, RNSessionSubscriber {
}

public func reload() {
session.reload()
session?.reload()
}

public func refresh() {
webView.evaluateJavaScript(REFRESH_SCRIPT)
webView?.evaluateJavaScript(REFRESH_SCRIPT)
}

private func visit() {
if (controller?.visitableURL?.absoluteString == url as String) {
return
}
controller!.visitableURL = URL(string: String(url))
session.visit(controller!)
session?.visit(controller!)
}

public func didProposeVisit(proposal: VisitProposal){
if (webView.url == proposal.url && proposal.options.action == .replace) {
if (webView?.url == proposal.url && proposal.options.action == .replace) {
// When reopening same URL we want to refresh webview
refresh()
} else {
Expand Down Expand Up @@ -191,7 +213,7 @@ class RNVisitableView: UIView, RNSessionSubscriber {
}

func clearSessionSnapshotCache(){
session.clearSnapshotCache()
session?.clearSnapshotCache()
}

func handleAlert(message: String, completionHandler: @escaping () -> Void) {
Expand All @@ -214,11 +236,12 @@ class RNVisitableView: UIView, RNSessionSubscriber {
extension RNVisitableView: RNVisitableViewControllerDelegate {

func visitableWillAppear(visitable: Visitable) {
session.visitableViewWillAppear(view: self)
session?.visitableViewWillAppear(view: self)
}

func visitableDidAppear(visitable: Visitable) {
session.visitableViewDidAppear(view: self)
configureWebView()
session?.visitableViewDidAppear(view: self)
}

func visitableDidDisappear(visitable: Visitable) {
Expand All @@ -227,8 +250,8 @@ extension RNVisitableView: RNVisitableViewControllerDelegate {

func visitableDidRender(visitable: Visitable) {
let event: [AnyHashable: Any] = [
"title": webView.title!,
"url": webView.url!
"title": webView!.title!,
"url": webView!.url!
]
onLoad?(event)
}
Expand Down
1 change: 1 addition & 0 deletions packages/turbo/ios/RNVisitableViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ @interface RCT_EXTERN_MODULE(RNVisitableViewManager, NSObject)
RCT_EXPORT_VIEW_PROPERTY(sessionHandle, NSString)
RCT_EXPORT_VIEW_PROPERTY(applicationNameForUserAgent, NSString)
RCT_EXPORT_VIEW_PROPERTY(pullToRefreshEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onVisitProposal, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onOpenExternalUrl, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
Expand Down
1 change: 1 addition & 0 deletions packages/turbo/src/RNVisitableView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface RNVisitableViewProps {
sessionHandle?: string;
applicationNameForUserAgent?: string;
pullToRefreshEnabled: boolean;
scrollEnabled: boolean;
onLoad?: (e: NativeSyntheticEvent<LoadEvent>) => void;
onMessage?: (e: NativeSyntheticEvent<MessageEvent>) => void;
onError?: (e: NativeSyntheticEvent<ErrorEvent>) => void;
Expand Down
Loading
Loading