feat(mobile): implement webview in android (#3442)
* feat(mobile): implement webview in android * fix(mobile): update Android resource paths to use 'assets' directory * fix(mobile): add injected JavaScript for WebView height management * feat(mobile): enhance WebView navigation handling
This commit is contained in:
parent
eec48fc5e8
commit
b295711f5e
|
|
@ -46,10 +46,10 @@ function addAndroidResources(config, { assetsPath }) {
|
|||
const { projectRoot } = config.modRequest
|
||||
|
||||
// Get the path to the Android resources directory
|
||||
const resDir = path.join(projectRoot, "android", "app", "src", "main", "res")
|
||||
const resDir = path.join(projectRoot, "android", "app", "src", "main")
|
||||
|
||||
// Create the 'raw' directory if it doesn't exist
|
||||
const rawDir = path.join(resDir, "raw")
|
||||
// Create the 'assets' directory if it doesn't exist
|
||||
const rawDir = path.join(resDir, "assets")
|
||||
fs.mkdirSync(rawDir, { recursive: true })
|
||||
|
||||
// Retrieve all files in the assets directory
|
||||
|
|
@ -65,6 +65,7 @@ function addAndroidResources(config, { assetsPath }) {
|
|||
// fs.copyFileSync(srcAssetPath, destAssetPath)
|
||||
// }
|
||||
|
||||
// Access the resources directory by using `file:///android_asset/FILENAME`
|
||||
return config
|
||||
},
|
||||
])
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ const assetPath = Image.resolveAssetSource({
|
|||
}).uri
|
||||
export const htmlUrl = Platform.select({
|
||||
ios: `file://${assetPath}/index.html`,
|
||||
android: "file:///android_asset/html-renderer/index.html",
|
||||
default: "",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { injectJavaScript } from "./native-webview.android"
|
||||
|
||||
export const SharedWebViewModule = {
|
||||
load: () => {
|
||||
console.warn("SharedWebViewModule.load is not implemented on Android")
|
||||
},
|
||||
evaluateJavaScript: () => {
|
||||
console.warn("SharedWebViewModule.evaluateJavaScript is not implemented on Android")
|
||||
evaluateJavaScript: (js: string) => {
|
||||
injectJavaScript(js)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
// Ported from apps/mobile/native/ios/Modules/SharedWebView/Injected/at_start.js
|
||||
|
||||
export const atStart = `
|
||||
;(() => {
|
||||
window.__RN__ = true
|
||||
|
||||
|
||||
function send(data) {
|
||||
window.ReactNativeWebView.postMessage?.(JSON.stringify(data))
|
||||
}
|
||||
|
||||
window.bridge = {
|
||||
measure: () => {
|
||||
send({
|
||||
type: "measure",
|
||||
})
|
||||
},
|
||||
setContentHeight: (height) => {
|
||||
send({
|
||||
type: "setContentHeight",
|
||||
payload: height,
|
||||
})
|
||||
},
|
||||
previewImage: (data) => {
|
||||
send({
|
||||
type: "previewImage",
|
||||
payload: {
|
||||
imageUrls: data.imageUrls,
|
||||
index: data.index || 0,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
})()
|
||||
`
|
||||
|
||||
export const atEnd = `
|
||||
|
||||
;(() => {
|
||||
const root = document.querySelector("#root")
|
||||
const handleHeight = () => {
|
||||
window.ReactNativeWebView.postMessage?.(
|
||||
JSON.stringify({
|
||||
type: "setContentHeight",
|
||||
payload: root.scrollHeight,
|
||||
}),
|
||||
)
|
||||
}
|
||||
window.addEventListener("load", handleHeight)
|
||||
const observer = new ResizeObserver(handleHeight)
|
||||
|
||||
setTimeout(() => {
|
||||
handleHeight()
|
||||
}, 1000)
|
||||
observer.observe(root)
|
||||
})()
|
||||
`
|
||||
|
|
@ -1,12 +1,100 @@
|
|||
import { jotaiStore } from "@follow/utils"
|
||||
import { atom } from "jotai"
|
||||
import type * as React from "react"
|
||||
import type { RefObject } from "react"
|
||||
import { useCallback, useRef } from "react"
|
||||
import type { ViewProps } from "react-native"
|
||||
import type { WebViewNavigation } from "react-native-webview"
|
||||
import WebView from "react-native-webview"
|
||||
|
||||
import { openLink } from "@/src/lib/native"
|
||||
|
||||
import { htmlUrl } from "./constants"
|
||||
import { atEnd, atStart } from "./injected-js"
|
||||
|
||||
const webviewAtom = atom<WebView | null>(null)
|
||||
|
||||
const setWebview = (webview: WebView | null) => {
|
||||
jotaiStore.set(webviewAtom, webview)
|
||||
}
|
||||
|
||||
export const injectJavaScript = (js: string) => {
|
||||
const webview = jotaiStore.get(webviewAtom)
|
||||
if (!webview) {
|
||||
console.warn("WebView not ready, injecting JavaScript failed", js)
|
||||
return
|
||||
}
|
||||
return webview.injectJavaScript(js)
|
||||
}
|
||||
|
||||
export const NativeWebView: React.ComponentType<
|
||||
ViewProps & {
|
||||
onContentHeightChange?: (e: { nativeEvent: { height: number } }) => void
|
||||
url?: string
|
||||
}
|
||||
> = () => {
|
||||
console.warn("NativeWebView is not implemented on this platform")
|
||||
return null
|
||||
> = ({ onContentHeightChange }) => {
|
||||
const webViewRef = useRef<WebView | null>(null)
|
||||
const { onNavigationStateChange } = useWebViewNavigation({ webViewRef })
|
||||
|
||||
return (
|
||||
<WebView
|
||||
ref={(webview) => {
|
||||
setWebview(webview)
|
||||
}}
|
||||
style={styles.webview}
|
||||
containerStyle={styles.webviewContainer}
|
||||
source={{ uri: htmlUrl }}
|
||||
// Open chrome://inspect/#devices, or Development menu on Safari to debug the WebView.
|
||||
// https://github.com/react-native-webview/react-native-webview/blob/master/docs/Debugging.md#debugging-webview-contents
|
||||
webviewDebuggingEnabled={__DEV__}
|
||||
sharedCookiesEnabled
|
||||
originWhitelist={["*"]}
|
||||
allowUniversalAccessFromFileURLs
|
||||
startInLoadingState
|
||||
allowsBackForwardNavigationGestures
|
||||
injectedJavaScriptBeforeContentLoaded={atStart}
|
||||
onNavigationStateChange={onNavigationStateChange}
|
||||
onLoadEnd={useCallback(() => {
|
||||
injectJavaScript(atEnd)
|
||||
}, [])}
|
||||
onMessage={(e) => {
|
||||
const message = e.nativeEvent.data
|
||||
const parsed = JSON.parse(message)
|
||||
if (parsed.type === "setContentHeight") {
|
||||
onContentHeightChange?.({
|
||||
nativeEvent: { height: parsed.payload },
|
||||
})
|
||||
return
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const useWebViewNavigation = ({ webViewRef }: { webViewRef: RefObject<WebView> }) => {
|
||||
const onNavigationStateChange = useCallback(
|
||||
(newNavState: WebViewNavigation) => {
|
||||
const { url: urlStr } = newNavState
|
||||
const url = URL.canParse(urlStr) ? new URL(urlStr) : null
|
||||
if (!url) return
|
||||
if (url.protocol === "file:") return
|
||||
// if (allowHosts.has(url.host)) return
|
||||
webViewRef.current?.stopLoading()
|
||||
// const formattedUrl = transformVideoUrl({ url: urlStr })
|
||||
if (urlStr) {
|
||||
openLink(urlStr)
|
||||
return
|
||||
}
|
||||
openLink(urlStr)
|
||||
},
|
||||
[webViewRef],
|
||||
)
|
||||
|
||||
return { onNavigationStateChange }
|
||||
}
|
||||
|
||||
const styles = {
|
||||
// https://github.com/react-native-webview/react-native-webview/issues/318#issuecomment-503979211
|
||||
webview: { backgroundColor: "transparent" },
|
||||
webviewContainer: { width: "100%", backgroundColor: "transparent" },
|
||||
} as const
|
||||
|
|
|
|||
Loading…
Reference in New Issue