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:
Whitewater 2025-04-11 21:18:03 +09:00 committed by GitHub
parent eec48fc5e8
commit b295711f5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 157 additions and 8 deletions

View File

@ -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
},
])

View File

@ -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: "",
})

View File

@ -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)
},
}

View File

@ -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)
})()
`

View File

@ -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