Skip to content

Commit

Permalink
feat: Implement the webview sandbox using a microfront end for rn to …
Browse files Browse the repository at this point in the history
…implement the full html renderer (#2691)

* init

Signed-off-by: Innei <tukon479@gmail.com>

* feat: parser and renderer

Signed-off-by: Innei <tukon479@gmail.com>

* feat: shared webview

* feat: add UIColor hex conversion extension and accent color asset and xhr rewrite

* feat(mobile): enhance WebView and HTML rendering for iOS

- Refactor SharedWebView module with improved JavaScript injection and link handling
- Add ModalWebViewController for opening links in a modal view
- Implement dynamic content height measurement
- Update HTML renderer with Jotai state management
- Improve WebView configuration and debug mode

Signed-off-by: Innei <tukon479@gmail.com>

* lockfile

Signed-off-by: Innei <tukon479@gmail.com>

* chore: auto-fix linting and formatting issues

* fix(mobile): improve WebView debug mode and UI text styling

- Remove debug-only conditional for WebView inspector
- Add text label class to GroupedInsetListActionCell for consistent styling

Signed-off-by: Innei <tukon479@gmail.com>

* update

Signed-off-by: Innei <tukon479@gmail.com>

* feat(mobile): add FullScreenWKWebView for iOS WebView layout

- Create custom WKWebView subclass to override safe area insets
- Enables full-screen WebView rendering without default padding

Signed-off-by: Innei <tukon479@gmail.com>

* chore(mobile): remove FullScreenWKWebView Swift file

- Delete unused custom WKWebView subclass for iOS
- Cleanup unnecessary file after previous implementation

Signed-off-by: Innei <tukon479@gmail.com>

* feat(mobile): refactor iOS WebView infrastructure

Signed-off-by: Innei <tukon479@gmail.com>

* feat(mobile): add native helper module and improve entry detail screen

- Add HelperModule for iOS to open links in a modal WebView
- Implement cross-platform link opening mechanism
- Enhance entry detail screen with dynamic title and scroll behavior
- Add loading indicator for entry content
- Refactor navigation and scroll view components

Signed-off-by: Innei <tukon479@gmail.com>

* fix(mobile): improve WebView JavaScript and URL loading thread safety

- Dispatch JavaScript evaluation and URL loading on the main thread
- Ensure thread-safe WebView interactions for iOS
- Prevent potential race conditions in SharedWebViewModule

Signed-off-by: Innei <tukon479@gmail.com>

* refactor(mobile): remove useOpenLink hook and update link opening mechanism

- Replace custom useOpenLink hook with native openLink function
- Update WebView navigation and recommendation list item to use new link opening method
- Remove unnecessary hook and simplify link handling

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
  • Loading branch information
Innei authored Feb 7, 2025
1 parent 2f3655c commit 64db32e
Show file tree
Hide file tree
Showing 65 changed files with 1,674 additions and 292 deletions.
3 changes: 2 additions & 1 deletion apps/mobile/native/example/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SharedWebView } from "follow-native"
import { ScrollView, View } from "react-native"

import { SharedWebView } from "@/src/components/native/webview"

export default function App() {
return (
<ScrollView>
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/native/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"platforms": ["apple", "android"],
"apple": {
"modules": ["SharedWebViewModule"]
"modules": ["SharedWebViewModule", "HelperModule"]
},
"android": {
"modules": []
Expand Down
58 changes: 0 additions & 58 deletions apps/mobile/native/ios/Expo+AutoSizingStack.swift

This file was deleted.

10 changes: 8 additions & 2 deletions apps/mobile/native/ios/FollowNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Pod::Spec.new do |s|
:tvos => '15.1'
}
s.swift_version = '5.4'
s.source = { git: 'https://github.com/Innei/follow-native' }
s.source = { git: 'https://github.com/RSSNext/follow' }
s.static_framework = true

s.dependency 'ExpoModulesCore'
Expand All @@ -25,5 +25,11 @@ Pod::Spec.new do |s|
'DEFINES_MODULE' => 'YES',
}

s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
s.source_files = "**/*.{h,m,mm,swift,hpp,cpp,js}"

s.resource_bundles = {
'js' => ['SharedWebView/injected/**/*'],
'FollowNative' => ['Media.xcassets'],
}

end
24 changes: 24 additions & 0 deletions apps/mobile/native/ios/Helper/HelperModule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// HelperModule.swift
// Pods
//
// Created by Innei on 2025/2/7.
//
import ExpoModulesCore

public class HelperModule: Module {
public func definition() -> ExpoModulesCore.ModuleDefinition {
Name("Helper")

Function("openLink") { (urlString: String) in
guard let url = URL(string: urlString) else {
return
}
DispatchQueue.main.async {
guard let rootVC = UIApplication.shared.windows.first?.rootViewController else { return }
WebViewManager.presentModalWebView(url: url, from: rootVC)
}

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0x00",
"green": "0x5C",
"red": "0xFF"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0x00",
"green": "0x5C",
"red": "0xFF"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
6 changes: 6 additions & 0 deletions apps/mobile/native/ios/Media.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
90 changes: 90 additions & 0 deletions apps/mobile/native/ios/SharedWebView/CustomURLSchemeHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// CustomURLSchemeHandler.swift
// Pods
//
// Created by Innei on 2025/2/7.
//

import WebKit
import Foundation


class CustomURLSchemeHandler: NSObject, WKURLSchemeHandler {
static let rewriteScheme = "follow-xhr"
private let queue = DispatchQueue(label: "com.follow.urlschemehandler")
private var activeTasks: [String: URLSessionDataTask] = [:]

func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let url = urlSchemeTask.request.url,
let originalURLString = url.absoluteString.replacingOccurrences(
of: CustomURLSchemeHandler.rewriteScheme, with: "https"
).removingPercentEncoding,
let originalURL = URL(string: originalURLString)
else {
urlSchemeTask.didFailWithError(NSError(domain: "", code: -1))
return
}

var request = URLRequest(url: originalURL)

request.httpMethod = urlSchemeTask.request.httpMethod
request.httpBody = urlSchemeTask.request.httpBody

// setting headers
var headers = urlSchemeTask.request.allHTTPHeaderFields ?? [:]
if let urlComponents = URLComponents(url: originalURL, resolvingAgainstBaseURL: false),
let scheme = urlComponents.scheme,
let host = urlComponents.host
{
let origin = "\(scheme)://\(host)\(urlComponents.port.map { ":\($0)" } ?? "")"
headers["Referer"] = origin
headers["Origin"] = origin

}
request.allHTTPHeaderFields = headers

let taskID = urlSchemeTask.description

let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let self = self else { return }

self.queue.sync {
// Check if task is still active
guard self.activeTasks[taskID] != nil else { return }

if let error = error {
urlSchemeTask.didFailWithError(error)
self.activeTasks.removeValue(forKey: taskID)
return
}

if let response = response as? HTTPURLResponse, let data = data {
do {
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
} catch {
// Ignore errors that might occur if task was stopped
print("Error completing URL scheme task: \(error)")
}
}
self.activeTasks.removeValue(forKey: taskID)
}
}
queue.sync {
activeTasks[taskID] = task
}

task.resume()
}

func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
let taskID = urlSchemeTask.description
queue.sync {
if let task = activeTasks[taskID] {
task.cancel()
activeTasks.removeValue(forKey: taskID)
}
}
}
}
35 changes: 35 additions & 0 deletions apps/mobile/native/ios/SharedWebView/FOWebView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// FOWebView.swift
// FollowNative
//
// Created by Innei on 2025/2/7.
//

import WebKit

class FOWebView: WKWebView {
private func setupView() {
scrollView.isScrollEnabled = false
scrollView.bounces = false
scrollView.contentInsetAdjustmentBehavior = .never

isOpaque = false
backgroundColor = UIColor.clear
scrollView.backgroundColor = UIColor.clear
tintColor = Utils.accentColor

if #available(iOS 16.4, *) {
isInspectable = true
}

}

override init(frame: CGRect, configuration: WKWebViewConfiguration) {
super.init(frame: frame, configuration: configuration)
setupView()
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
25 changes: 25 additions & 0 deletions apps/mobile/native/ios/SharedWebView/Injected/at_end.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// at_end.js
// Pods
//
// Created by Innei on 2025/2/6.
//

;(() => {
const root = document.querySelector("#root")
const handleHeight = () => {
window.webkit.messageHandlers.message.postMessage(
JSON.stringify({
type: "setContentHeight",
payload: root.scrollHeight,
}),
)
}
window.addEventListener("load", handleHeight)
const observer = new ResizeObserver(handleHeight)

setTimeout(() => {
handleHeight()
}, 1000)
observer.observe(root)
})()
27 changes: 27 additions & 0 deletions apps/mobile/native/ios/SharedWebView/Injected/at_start.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// at_start.js
// Pods
//
// Created by Innei on 2025/2/6.
//
;(() => {
window.__RN__ = true

function send(data) {
window.webkit.messageHandlers.message.postMessage?.(JSON.stringify(data))
}

Object.assign(window.webkit, {
measure: () => {
send({
type: "measure",
})
},
setContentHeight: (height) => {
send({
type: "setContentHeight",
payload: height,
})
},
})
})()
Loading

0 comments on commit 64db32e

Please sign in to comment.