refactor(mobile): Improve WebView architecture and state management
Refactored the WebView implementation in the mobile app to: - Separate concerns in WebView delegate and configuration - Simplify WebView initialization and state management - Improve JavaScript injection and custom URL scheme handling - Add content height change event support Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
162f372d60
commit
11b4027dfd
|
|
@ -5,7 +5,9 @@
|
|||
// Created by Innei on 2025/2/7.
|
||||
//
|
||||
|
||||
import WebKit
|
||||
@preconcurrency import WebKit
|
||||
|
||||
private var pendingJavaScripts: [String] = []
|
||||
|
||||
class FOWebView: WKWebView {
|
||||
private func setupView() {
|
||||
|
|
@ -24,12 +26,237 @@ class FOWebView: WKWebView {
|
|||
|
||||
}
|
||||
|
||||
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
|
||||
private let delegate: WebViewDelegate!
|
||||
|
||||
init(frame: CGRect, state: WebViewState) {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
let viewController = Utils.getRootVC()!
|
||||
self.delegate = WebViewDelegate(state: state, viewController: viewController)
|
||||
|
||||
super.init(frame: frame, configuration: configuration)
|
||||
|
||||
let bundle = Utils.bundle
|
||||
|
||||
let hexAccentColor = Utils.accentColor.toHex()
|
||||
let css = """
|
||||
:root { overflow: hidden !important; overflow-behavior: none !important; }
|
||||
body {
|
||||
overflow-y: visible !important;
|
||||
position: absolute !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
::selection {
|
||||
background-color: \(hexAccentColor) !important;
|
||||
}
|
||||
"""
|
||||
|
||||
let script = WKUserScript(
|
||||
source: """
|
||||
var style = document.createElement('style');
|
||||
style.textContent = '\(css)';
|
||||
document.head.appendChild(style);
|
||||
""",
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
|
||||
let atStartScripts = loadInjectedJs(forResource: "at_start")
|
||||
|
||||
if let jsString = atStartScripts {
|
||||
let script = WKUserScript(
|
||||
source: jsString,
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
configuration.userContentController.addUserScript(script)
|
||||
}
|
||||
|
||||
configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
|
||||
|
||||
let schemeHandler = CustomURLSchemeHandler()
|
||||
configuration.setURLSchemeHandler(
|
||||
schemeHandler, forURLScheme: CustomURLSchemeHandler.rewriteScheme)
|
||||
|
||||
let customSchemeScript = WKUserScript(
|
||||
source: """
|
||||
(function() {
|
||||
const originalXHROpen = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function(method, url, ...args) {
|
||||
const modifiedUrl = url.replace(/^https?:/, '\(CustomURLSchemeHandler.rewriteScheme):');
|
||||
originalXHROpen.call(this, method, modifiedUrl, ...args);
|
||||
};
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(url, options) {
|
||||
const modifiedUrl = url.replace(/^https?:/, '\(CustomURLSchemeHandler.rewriteScheme):');
|
||||
return originalFetch(modifiedUrl, options);
|
||||
};
|
||||
|
||||
const originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, 'src');
|
||||
Object.defineProperty(Image.prototype, 'src', {
|
||||
set: function(url) {
|
||||
const modifiedUrl = url.replace(/^https?:/, '\(CustomURLSchemeHandler.rewriteScheme):');
|
||||
originalImageSrc.set.call(this, modifiedUrl);
|
||||
}
|
||||
});
|
||||
})();
|
||||
""",
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: false
|
||||
)
|
||||
configuration.userContentController.addUserScript(customSchemeScript)
|
||||
|
||||
let atEndScripts = self.loadInjectedJs(forResource: "at_end")
|
||||
guard let jsString = atEndScripts else {
|
||||
print("Failed to load injected js")
|
||||
return
|
||||
}
|
||||
let script2 = WKUserScript(
|
||||
source: jsString,
|
||||
injectionTime: .atDocumentEnd,
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
|
||||
configuration.userContentController.add(delegate, name: "message")
|
||||
configuration.userContentController.addUserScript(script2)
|
||||
|
||||
setupView()
|
||||
navigationDelegate = delegate
|
||||
uiDelegate = delegate
|
||||
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension FOWebView {
|
||||
|
||||
private func loadInjectedJs(forResource: String) -> String? {
|
||||
if let bundleURL = Bundle(for: WebViewView.self).url(
|
||||
forResource: "js", withExtension: "bundle"),
|
||||
let resourceBundle = Bundle(url: bundleURL)
|
||||
{
|
||||
|
||||
if let jsPath = resourceBundle.path(forResource: forResource, ofType: "js") {
|
||||
do {
|
||||
let initJsContent = try String(contentsOfFile: jsPath, encoding: .utf8)
|
||||
|
||||
return initJsContent
|
||||
} catch {
|
||||
print("Error reading JS file:", error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class WebViewDelegate: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate
|
||||
{
|
||||
private let state: WebViewState
|
||||
private weak var viewController: UIViewController?
|
||||
|
||||
init(state: WebViewState, viewController: UIViewController?) {
|
||||
self.state = state
|
||||
self.viewController = viewController
|
||||
super.init()
|
||||
}
|
||||
|
||||
func userContentController(
|
||||
_ userContentController: WKUserContentController, didReceive message: WKScriptMessage
|
||||
) {
|
||||
debugPrint("message", message.body)
|
||||
if message.name == "message" {
|
||||
let body = message.body
|
||||
|
||||
if let jsonString = body as? String, let decode = jsonString.data(using: .utf8) {
|
||||
let data = try? JSONDecoder().decode(BridgeDataBasePayload.self, from: decode)
|
||||
guard let data = data else { return }
|
||||
|
||||
switch data.type {
|
||||
case "setContentHeight":
|
||||
let data = try? JSONDecoder().decode(
|
||||
SetContentHeightPayload.self, from: decode)
|
||||
guard let data = data else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.state.contentHeight = data.payload
|
||||
}
|
||||
|
||||
case "measure":
|
||||
self.measureWebView(SharedWebViewModule.sharedWebView!)
|
||||
|
||||
case "previewImage":
|
||||
let data = try? JSONDecoder().decode(
|
||||
PreviewImagePayload.self, from: decode)
|
||||
|
||||
guard let data = data else { return }
|
||||
DispatchQueue.main.async {
|
||||
ImagePreview.quickLookImage(
|
||||
data.payload.images.compactMap { Data($0) })
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func measureWebView(_ webView: WKWebView) {
|
||||
let jsCode = "document.querySelector('#root').scrollHeight"
|
||||
webView.evaluateJavaScript(jsCode) { (height, error) in
|
||||
if let height = height as? CGFloat, height > 0 {
|
||||
DispatchQueue.main.async {
|
||||
self.state.contentHeight = height
|
||||
debugPrint("measure", height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
measureWebView(webView)
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
|
||||
self?.measureWebView(webView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
|
||||
for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures
|
||||
) -> WKWebView? {
|
||||
if let url = navigationAction.request.url,
|
||||
let viewController = self.viewController
|
||||
{
|
||||
WebViewManager.presentModalWebView(url: url, from: viewController)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
|
||||
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
|
||||
) {
|
||||
if navigationAction.targetFrame == nil {
|
||||
if let url = navigationAction.request.url,
|
||||
let viewController = self.viewController
|
||||
{
|
||||
WebViewManager.presentModalWebView(url: url, from: viewController)
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
}
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
//
|
||||
// File.swift
|
||||
// SharedWebViewModule.swift
|
||||
//
|
||||
// Created by Innei on 2025/1/29.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import ExpoModulesCore
|
||||
import WebKit
|
||||
|
||||
let onContentHeightChanged = "onContentHeightChanged"
|
||||
|
||||
public class SharedWebViewModule: Module {
|
||||
private var pendingJavaScripts: [String] = []
|
||||
|
||||
|
|
@ -38,6 +41,19 @@ public class SharedWebViewModule: Module {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Events(onContentHeightChanged)
|
||||
var cancellable: AnyCancellable?
|
||||
OnStartObserving {
|
||||
cancellable = WebViewManager.state.$contentHeight
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] height in
|
||||
self?.sendEvent(onContentHeightChanged, ["height": height])
|
||||
}
|
||||
}
|
||||
OnStopObserving {
|
||||
cancellable?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private func load(urlString: String) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
import Combine
|
||||
import ExpoModulesCore
|
||||
import SafariServices
|
||||
|
||||
@preconcurrency import WebKit
|
||||
|
||||
private var pendingJavaScripts: [String] = []
|
||||
|
|
@ -18,8 +17,7 @@ protocol WebViewLinkDelegate: AnyObject {
|
|||
}
|
||||
|
||||
enum WebViewManager {
|
||||
static let state = WebViewState()
|
||||
static weak var linkDelegate: WebViewLinkDelegate?
|
||||
static var state = WebViewState()
|
||||
|
||||
public static func evaluateJavaScript(_ js: String) {
|
||||
DispatchQueue.main.async {
|
||||
|
|
@ -43,253 +41,45 @@ enum WebViewManager {
|
|||
}
|
||||
|
||||
static private(set) var shared: WKWebView = {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
let bundle = Utils.bundle
|
||||
FOWebView(frame: .zero, state: state)
|
||||
|
||||
let hexAccentColor = Utils.accentColor.toHex()
|
||||
let css = """
|
||||
:root { overflow: hidden !important; overflow-behavior: none !important; }
|
||||
body {
|
||||
overflow-y: visible !important;
|
||||
position: absolute !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
}
|
||||
::selection {
|
||||
background-color: \(hexAccentColor) !important;
|
||||
}
|
||||
"""
|
||||
|
||||
let script = WKUserScript(
|
||||
source: """
|
||||
var style = document.createElement('style');
|
||||
style.textContent = '\(css)';
|
||||
document.head.appendChild(style);
|
||||
""",
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
|
||||
let jsString = loadInjectedJs(forResource: "at_start")
|
||||
|
||||
if let jsString = jsString {
|
||||
let script = WKUserScript(
|
||||
source: jsString,
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
configuration.userContentController.addUserScript(script)
|
||||
}
|
||||
|
||||
configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
|
||||
|
||||
let schemeHandler = CustomURLSchemeHandler()
|
||||
configuration.setURLSchemeHandler(
|
||||
schemeHandler, forURLScheme: CustomURLSchemeHandler.rewriteScheme)
|
||||
|
||||
let customSchemeScript = WKUserScript(
|
||||
source: """
|
||||
(function() {
|
||||
const originalXHROpen = XMLHttpRequest.prototype.open;
|
||||
XMLHttpRequest.prototype.open = function(method, url, ...args) {
|
||||
const modifiedUrl = url.replace(/^https?:/, '\(CustomURLSchemeHandler.rewriteScheme):');
|
||||
originalXHROpen.call(this, method, modifiedUrl, ...args);
|
||||
};
|
||||
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(url, options) {
|
||||
const modifiedUrl = url.replace(/^https?:/, '\(CustomURLSchemeHandler.rewriteScheme):');
|
||||
return originalFetch(modifiedUrl, options);
|
||||
};
|
||||
|
||||
const originalImageSrc = Object.getOwnPropertyDescriptor(Image.prototype, 'src');
|
||||
Object.defineProperty(Image.prototype, 'src', {
|
||||
set: function(url) {
|
||||
const modifiedUrl = url.replace(/^https?:/, '\(CustomURLSchemeHandler.rewriteScheme):');
|
||||
originalImageSrc.set.call(this, modifiedUrl);
|
||||
}
|
||||
});
|
||||
})();
|
||||
""",
|
||||
injectionTime: .atDocumentStart,
|
||||
forMainFrameOnly: false
|
||||
)
|
||||
configuration.userContentController.addUserScript(customSchemeScript)
|
||||
|
||||
let webView = FOWebView(frame: .zero, configuration: configuration)
|
||||
|
||||
setupWebView(webView)
|
||||
|
||||
return webView
|
||||
}()
|
||||
|
||||
private static func setupWebView(_ webView: WKWebView) {
|
||||
guard let viewController = Utils.getRootVC() else { return }
|
||||
let delegate = WebViewDelegate(state: state, viewController: viewController)
|
||||
webView.navigationDelegate = delegate
|
||||
webView.uiDelegate = delegate
|
||||
|
||||
let jsString = self.loadInjectedJs(forResource: "at_end")
|
||||
guard let jsString = jsString else {
|
||||
print("Failed to load injected js")
|
||||
return
|
||||
}
|
||||
let script = WKUserScript(
|
||||
source: jsString,
|
||||
injectionTime: .atDocumentEnd,
|
||||
forMainFrameOnly: true
|
||||
)
|
||||
|
||||
webView.configuration.userContentController.add(delegate, name: "message")
|
||||
webView.configuration.userContentController.addUserScript(script)
|
||||
}
|
||||
|
||||
static func resetWebView() {
|
||||
let newWebView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
|
||||
setupWebView(newWebView)
|
||||
shared = newWebView
|
||||
}
|
||||
|
||||
private static func loadInjectedJs(forResource: String) -> String? {
|
||||
if let bundleURL = Bundle(for: WebViewView.self).url(
|
||||
forResource: "js", withExtension: "bundle"),
|
||||
let resourceBundle = Bundle(url: bundleURL)
|
||||
{
|
||||
|
||||
if let jsPath = resourceBundle.path(forResource: forResource, ofType: "js") {
|
||||
do {
|
||||
let initJsContent = try String(contentsOfFile: jsPath, encoding: .utf8)
|
||||
|
||||
return initJsContent
|
||||
} catch {
|
||||
print("Error reading JS file:", error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
self.state = WebViewState()
|
||||
self.shared = FOWebView(frame: .zero, state: state)
|
||||
}
|
||||
|
||||
static func presentModalWebView(url: URL, from viewController: UIViewController) {
|
||||
|
||||
let safariVC = SFSafariViewController(url: url)
|
||||
safariVC.view.tintColor = Utils.accentColor
|
||||
safariVC.preferredControlTintColor = Utils.accentColor
|
||||
viewController.present(safariVC, animated: true)
|
||||
let safariVC = SFSafariViewController(url: url)
|
||||
safariVC.view.tintColor = Utils.accentColor
|
||||
safariVC.preferredControlTintColor = Utils.accentColor
|
||||
viewController.present(safariVC, animated: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class WebViewDelegate: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate
|
||||
{
|
||||
private let state: WebViewState
|
||||
private weak var viewController: UIViewController?
|
||||
|
||||
init(state: WebViewState, viewController: UIViewController?) {
|
||||
self.state = state
|
||||
self.viewController = viewController
|
||||
super.init()
|
||||
}
|
||||
|
||||
func userContentController(
|
||||
_ userContentController: WKUserContentController, didReceive message: WKScriptMessage
|
||||
) {
|
||||
debugPrint("message", message.body)
|
||||
if message.name == "message" {
|
||||
let body = message.body
|
||||
|
||||
if let jsonString = body as? String, let decode = jsonString.data(using: .utf8) {
|
||||
let data = try? JSONDecoder().decode(BridgeDataBasePayload.self, from: decode)
|
||||
guard let data = data else { return }
|
||||
|
||||
switch data.type {
|
||||
case "setContentHeight":
|
||||
let data = try? JSONDecoder().decode(
|
||||
SetContentHeightPayload.self, from: decode)
|
||||
guard let data = data else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.state.contentHeight = data.payload
|
||||
}
|
||||
|
||||
case "measure":
|
||||
self.measureWebView(SharedWebViewModule.sharedWebView!)
|
||||
|
||||
case "previewImage":
|
||||
let data = try? JSONDecoder().decode(
|
||||
PreviewImagePayload.self, from: decode)
|
||||
|
||||
guard let data = data else { return }
|
||||
DispatchQueue.main.async {
|
||||
ImagePreview.quickLookImage(
|
||||
data.payload.images.compactMap { Data($0) })
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func measureWebView(_ webView: WKWebView) {
|
||||
let jsCode = "document.querySelector('#root').scrollHeight"
|
||||
webView.evaluateJavaScript(jsCode) { (height, error) in
|
||||
if let height = height as? CGFloat, height > 0 {
|
||||
DispatchQueue.main.async {
|
||||
self.state.contentHeight = height
|
||||
debugPrint("measure", height)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
measureWebView(webView)
|
||||
|
||||
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
|
||||
self?.measureWebView(webView)
|
||||
}
|
||||
|
||||
for js in pendingJavaScripts {
|
||||
webView.evaluateJavaScript(js) { (_, error) in
|
||||
if let error = error {
|
||||
print("Error evaluating JavaScript:", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
pendingJavaScripts.removeAll()
|
||||
|
||||
}
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
|
||||
for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures
|
||||
) -> WKWebView? {
|
||||
if let url = navigationAction.request.url,
|
||||
let viewController = self.viewController
|
||||
{
|
||||
WebViewManager.presentModalWebView(url: url, from: viewController)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func webView(
|
||||
_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
|
||||
decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
|
||||
) {
|
||||
if navigationAction.targetFrame == nil {
|
||||
if let url = navigationAction.request.url,
|
||||
let viewController = self.viewController
|
||||
{
|
||||
WebViewManager.presentModalWebView(url: url, from: viewController)
|
||||
decisionHandler(.cancel)
|
||||
return
|
||||
}
|
||||
}
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
}
|
||||
//
|
||||
//extension WebViewManager {
|
||||
// private static var contentHeightChangeListeners: [Int: (Float) -> Void] = [:]
|
||||
// private static var contentHeightChangeListenerId = 0
|
||||
//
|
||||
// static func addContentHeightChangeListener(_ listener: @escaping (Float) -> Void) -> Int {
|
||||
// let listenerId = contentHeightChangeListenerId
|
||||
// contentHeightChangeListenerId += 1
|
||||
// contentHeightChangeListeners[listenerId] = listener
|
||||
// return listenerId
|
||||
// }
|
||||
//
|
||||
// static func removeContentHeightChangeListener(_ listenerId: Int) {
|
||||
// contentHeightChangeListeners.removeValue(forKey: listenerId)
|
||||
// }
|
||||
//
|
||||
//// static func notifyContentHeightChange(_ height: Float) {
|
||||
//// for listener in contentHeightChangeListeners.values {
|
||||
//// DispatchQueue.main.async {
|
||||
//// listener(height)
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { clsx } from "@follow/utils"
|
||||
import { requireNativeView } from "expo"
|
||||
import { useAtom } from "jotai"
|
||||
import * as React from "react"
|
||||
import { useEffect } from "react"
|
||||
import type { ViewProps } from "react-native"
|
||||
|
|
@ -10,6 +11,7 @@ import { BugCuteReIcon } from "@/src/icons/bug_cute_re"
|
|||
import type { EntryModel } from "@/src/store/entry/types"
|
||||
|
||||
import { Portal } from "../../ui/portal"
|
||||
import { sharedWebViewHeightAtom } from "./atom"
|
||||
import { htmlUrl } from "./constants"
|
||||
import { prepareEntryRenderWebView, SharedWebViewModule } from "./index"
|
||||
|
||||
|
|
@ -31,11 +33,12 @@ const setCodeTheme = (light: string, dark: string) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const setWebViewEntry = (entry: EntryModel) => {
|
||||
const setWebViewEntry = (entry: EntryModel) => {
|
||||
SharedWebViewModule.evaluateJavaScript(
|
||||
`setEntry(JSON.parse(${JSON.stringify(JSON.stringify(entry))}))`,
|
||||
)
|
||||
}
|
||||
export { setWebViewEntry as preloadWebViewEntry }
|
||||
|
||||
const setNoMedia = (value: boolean) => {
|
||||
SharedWebViewModule.evaluateJavaScript(`setNoMedia(${value})`)
|
||||
|
|
@ -46,7 +49,7 @@ const setReaderRenderInlineStyle = (value: boolean) => {
|
|||
}
|
||||
|
||||
export function EntryContentWebView(props: EntryContentWebViewProps) {
|
||||
const [contentHeight, setContentHeight] = React.useState(0)
|
||||
const [contentHeight, setContentHeight] = useAtom(sharedWebViewHeightAtom)
|
||||
|
||||
const codeThemeLight = useUISettingKey("codeHighlightThemeLight")
|
||||
const codeThemeDark = useUISettingKey("codeHighlightThemeDark")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
import { atom } from "jotai"
|
||||
import { Dimensions } from "react-native"
|
||||
|
||||
export const sharedWebViewHeightAtom = atom<number>(Dimensions.get("window").height)
|
||||
|
|
@ -3,13 +3,21 @@ import { requireNativeModule } from "expo-modules-core"
|
|||
|
||||
import { htmlUrl } from "./constants"
|
||||
|
||||
declare class ISharedWebViewModule extends NativeModule {
|
||||
declare class ISharedWebViewModule extends NativeModule<{
|
||||
onContentHeightChanged: ({ height }: { height: number }) => void
|
||||
}> {
|
||||
load(url: string): void
|
||||
evaluateJavaScript(js: string): void
|
||||
}
|
||||
|
||||
export const SharedWebViewModule = requireNativeModule<ISharedWebViewModule>("FOSharedWebView")
|
||||
|
||||
let prepareOnce = false
|
||||
export const prepareEntryRenderWebView = () => {
|
||||
if (prepareOnce) return
|
||||
prepareOnce = true
|
||||
SharedWebViewModule.load(htmlUrl)
|
||||
// SharedWebViewModule.addListener("onContentHeightChanged", ({ height }) => {
|
||||
// jotaiStore.set(sharedWebViewHeightAtom, height)
|
||||
// })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Share, Text, View } from "react-native"
|
|||
|
||||
import {
|
||||
EntryContentWebView,
|
||||
setWebViewEntry,
|
||||
preloadWebViewEntry,
|
||||
} from "@/src/components/native/webview/EntryContentWebView"
|
||||
import { ContextMenu } from "@/src/components/ui/context-menu"
|
||||
import { PortalHost } from "@/src/components/ui/portal"
|
||||
|
|
@ -25,7 +25,7 @@ export const EntryItemContextMenu = ({ id, children }: PropsWithChildren<{ id: s
|
|||
|
||||
const handlePressPreview = useCallback(() => {
|
||||
if (!entry) return
|
||||
setWebViewEntry(entry)
|
||||
preloadWebViewEntry(entry)
|
||||
router.push(`/entries/${id}`)
|
||||
}, [entry, id])
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useCallback, useEffect } from "react"
|
|||
import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native"
|
||||
import ReAnimated, { useAnimatedStyle, useSharedValue, withSpring } from "react-native-reanimated"
|
||||
|
||||
import { setWebViewEntry } from "@/src/components/native/webview/EntryContentWebView"
|
||||
import { preloadWebViewEntry } from "@/src/components/native/webview/EntryContentWebView"
|
||||
import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime"
|
||||
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
|
||||
import { ProxiedImage } from "@/src/components/ui/image/ProxiedImage"
|
||||
|
|
@ -27,7 +27,7 @@ export function EntryNormalItem({ entryId, extraData }: { entryId: string; extra
|
|||
|
||||
const handlePress = useCallback(() => {
|
||||
if (!entry) return
|
||||
setWebViewEntry(entry)
|
||||
preloadWebViewEntry(entry)
|
||||
router.push(`/entries/${entryId}`)
|
||||
}, [entryId, entry])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue