diff --git a/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift b/apps/mobile/native/ios/Controllers/ModalWebViewController.swift
similarity index 81%
rename from apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift
rename to apps/mobile/native/ios/Controllers/ModalWebViewController.swift
index 5d290ff82..379b6e894 100644
--- a/apps/mobile/native/ios/SharedWebView/ModalWebViewController.swift
+++ b/apps/mobile/native/ios/Controllers/ModalWebViewController.swift
@@ -27,6 +27,13 @@ class ModalWebViewController: UIViewController {
setupNavigationBar()
setupWebView()
loadContent()
+ setupInteractivePopGesture()
+
+ }
+
+ private func setupInteractivePopGesture() {
+ navigationController?.interactivePopGestureRecognizer?.delegate = self
+ navigationController?.interactivePopGestureRecognizer?.isEnabled = true
}
private func setupNavigationBar() {
@@ -55,6 +62,7 @@ class ModalWebViewController: UIViewController {
private func setupWebView() {
view.addSubview(webView)
+ view.backgroundColor = .systemBackground
webView.snp.makeConstraints { make in
make.top.equalTo(view.safeAreaLayoutGuide)
make.left.right.bottom.equalToSuperview()
@@ -81,3 +89,10 @@ extension ModalWebViewController: WKNavigationDelegate {
navigationItem.title = webView.title
}
}
+
+// MARK: - UIGestureRecognizerDelegate
+extension ModalWebViewController: UIGestureRecognizerDelegate {
+ func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ return true
+ }
+}
diff --git a/apps/mobile/native/ios/Controllers/WebViewController.swift b/apps/mobile/native/ios/Controllers/WebViewController.swift
new file mode 100644
index 000000000..363f87df8
--- /dev/null
+++ b/apps/mobile/native/ios/Controllers/WebViewController.swift
@@ -0,0 +1,150 @@
+//
+// WebViewController.swift
+// FollowNative
+//
+// Created by Innei on 2025/2/27.
+//
+
+import Foundation
+import SnapKit
+import UIKit
+import WebKit
+
+class WebViewController: UIViewController {
+ private let webView: WKWebView
+ private let url: URL
+
+ init(url: URL) {
+ self.url = url
+
+ let configuration = WKWebViewConfiguration()
+
+ self.webView = WKWebView(frame: .zero, configuration: configuration)
+ super.init(nibName: nil, bundle: nil)
+
+ webView.navigationDelegate = self
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ setupWebView()
+ setupToolbar()
+ loadContent()
+ setupInteractivePopGesture()
+ }
+
+ private func setupInteractivePopGesture() {
+ navigationController?.interactivePopGestureRecognizer?.delegate = self
+ navigationController?.interactivePopGestureRecognizer?.isEnabled = true
+ }
+
+ private func setupWebView() {
+ view.addSubview(webView)
+ view.backgroundColor = .systemBackground
+ webView.snp.makeConstraints { make in
+ make.top.equalTo(view.safeAreaLayoutGuide)
+ make.left.right.bottom.equalToSuperview()
+ }
+ }
+
+ private func setupToolbar() {
+ // 确保导航控制器显示工具栏
+ navigationController?.isToolbarHidden = false
+
+ // 创建工具栏按钮
+ let backButton = UIBarButtonItem(
+ image: UIImage(systemName: "chevron.backward"), style: .plain, target: self,
+ action: #selector(goBack))
+ backButton.tintColor = Utils.accentColor
+
+ let forwardButton = UIBarButtonItem(
+ image: UIImage(systemName: "chevron.forward"), style: .plain, target: self,
+ action: #selector(goForward))
+ forwardButton.tintColor = Utils.accentColor
+
+ let refreshButton = UIBarButtonItem(
+ barButtonSystemItem: .refresh, target: self, action: #selector(refreshPage))
+ refreshButton.tintColor = Utils.accentColor
+
+ let safariButton = UIBarButtonItem(
+ image: UIImage(systemName: "safari"), style: .plain, target: self,
+ action: #selector(openInSafari))
+ safariButton.tintColor = Utils.accentColor
+
+ // 添加弹性空间使按钮均匀分布
+ let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
+
+ // 设置工具栏项目
+ toolbarItems = [
+ flexSpace, backButton, flexSpace, forwardButton, flexSpace, refreshButton, flexSpace,
+ safariButton, flexSpace,
+ ]
+ }
+
+ private func loadContent() {
+ let request = URLRequest(url: url)
+ webView.load(request)
+ }
+
+ @objc private func openInSafari() {
+ UIApplication.shared.open(url)
+ }
+
+ @objc private func goBack() {
+ if webView.canGoBack {
+ webView.goBack()
+ }
+ }
+
+ @objc private func goForward() {
+ if webView.canGoForward {
+ webView.goForward()
+ }
+ }
+
+ @objc private func refreshPage() {
+ webView.reload()
+ }
+}
+
+// MARK: - WKNavigationDelegate
+extension WebViewController: WKNavigationDelegate {
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
+ navigationItem.title = webView.title
+
+ // 更新后退/前进按钮状态
+ updateToolbarButtonsState()
+ }
+
+ func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
+ // 更新后退/前进按钮状态
+ updateToolbarButtonsState()
+ }
+
+ private func updateToolbarButtonsState() {
+ // 找到后退和前进按钮并更新它们的启用状态
+ if let items = toolbarItems {
+ // 后退按钮在索引1
+ if let backButton = items[1] as? UIBarButtonItem {
+ backButton.isEnabled = webView.canGoBack
+ }
+
+ // 前进按钮在索引3
+ if let forwardButton = items[3] as? UIBarButtonItem {
+ forwardButton.isEnabled = webView.canGoForward
+ }
+ }
+ }
+}
+
+// MARK: - UIGestureRecognizerDelegate
+extension WebViewController: UIGestureRecognizerDelegate {
+ func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
+ return true
+ }
+}
diff --git a/apps/mobile/native/ios/Helper/HelperModule.swift b/apps/mobile/native/ios/Helper/HelperModule.swift
index f0a704fc9..5c00775cc 100644
--- a/apps/mobile/native/ios/Helper/HelperModule.swift
+++ b/apps/mobile/native/ios/Helper/HelperModule.swift
@@ -16,8 +16,8 @@ public class HelperModule: Module {
return
}
DispatchQueue.main.async {
- guard let rootVC = UIApplication.shared.windows.first?.rootViewController else { return }
- WebViewManager.presentModalWebView(url: url, from: rootVC)
+// guard let rootVC = UIApplication.shared.windows.first?.rootViewController else { return }
+// WebViewManager.pushModalWebView(url: url, from: rootVC)
}
}
diff --git a/apps/mobile/native/ios/SharedWebView/WebViewManager.swift b/apps/mobile/native/ios/SharedWebView/WebViewManager.swift
index 92f597939..8b37d0f9e 100644
--- a/apps/mobile/native/ios/SharedWebView/WebViewManager.swift
+++ b/apps/mobile/native/ios/SharedWebView/WebViewManager.swift
@@ -175,6 +175,11 @@ enum WebViewManager {
navController.modalPresentationStyle = .fullScreen
viewController.present(navController, animated: true)
}
+
+ static func pushModalWebView(url: URL, from navigationController: UINavigationController) {
+ let modalVC = ModalWebViewController(url: url)
+ navigationController.pushViewController(modalVC, animated: true)
+ }
}
private class WebViewDelegate: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate
diff --git a/apps/mobile/src/components/layouts/header/NavigationHeader.tsx b/apps/mobile/src/components/layouts/header/NavigationHeader.tsx
index 9333765e9..4cc2bcf47 100644
--- a/apps/mobile/src/components/layouts/header/NavigationHeader.tsx
+++ b/apps/mobile/src/components/layouts/header/NavigationHeader.tsx
@@ -147,10 +147,12 @@ export const NavigationHeader = ({
const setHeaderHeight = useContext(SetNavigationHeaderHeightContext)
useEffect(() => {
- const id = scrollY.addListener(({ value }) => {
+ const handler = ({ value }: { value: number }) => {
opacityAnimated.value = Math.max(0, Math.min(1, (value + blurThreshold) / 10))
- })
+ }
+ const id = scrollY.addListener(handler)
+ handler({ value: (scrollY as any)._value })
return () => {
scrollY.removeListener(id)
}
@@ -191,7 +193,7 @@ export const NavigationHeader = ({
}
}, [navigation, title])
- const HeaderLeft = headerLeft ?? DefaultHeaderLeft
+ const HeaderLeft = headerLeft ?? DefaultHeaderBackButton
const renderTitle = customHeaderTitle ?? HeaderTitle
const headerTitle =
@@ -306,14 +308,27 @@ export const NavigationHeader = ({
)
}
-const DefaultHeaderLeft = ({ canGoBack }: { canGoBack: boolean }) => {
+export const DefaultHeaderBackButton = ({ canGoBack }: { canGoBack: boolean }) => {
const label = useColor("label")
if (!canGoBack) return null
return (
- router.back()}>
+ router.back()}>
-
+
)
}
+export const UINavigationHeaderActionButton = ({
+ children,
+ onPress,
+}: {
+ children: ReactNode
+ onPress?: () => void
+}) => {
+ return (
+
+ {children}
+
+ )
+}
const Noop = () => null
diff --git a/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx b/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx
index ee6406e47..3cddc03c7 100644
--- a/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx
+++ b/apps/mobile/src/components/layouts/views/SafeNavigationScrollView.tsx
@@ -1,21 +1,19 @@
import { useTypeScriptHappyCallback } from "@follow/hooks"
import { getDefaultHeaderHeight } from "@react-navigation/elements"
import type { NativeStackNavigationOptions } from "@react-navigation/native-stack"
-import { router, Stack, useNavigation } from "expo-router"
+import { Stack } from "expo-router"
import type { FC, PropsWithChildren } from "react"
import { useContext, useEffect, useMemo, useRef, useState } from "react"
import type { ScrollView, ScrollViewProps } from "react-native"
-import { Animated as RNAnimated, TouchableOpacity, useAnimatedValue, View } from "react-native"
+import { Animated as RNAnimated, useAnimatedValue, View } from "react-native"
import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes"
import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context"
-import { useColor } from "react-native-uikit-colors"
import {
AttachNavigationScrollViewContext,
SetAttachNavigationScrollViewContext,
} from "@/src/components/layouts/tabbar/contexts/AttachNavigationScrollViewContext"
import { useBottomTabBarHeight } from "@/src/components/layouts/tabbar/hooks"
-import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line"
import { AnimatedScrollView } from "../../common/AnimatedComponents"
import { NavigationHeader } from "../header/NavigationHeader"
@@ -104,8 +102,6 @@ export const NavigationBlurEffectHeader = ({
headerHideableBottom?: () => React.ReactNode
headerTitleAbsolute?: boolean
}) => {
- const canBack = useNavigation().canGoBack()
-
const navigationContext = useContext(NavigationContext)!
const setHeaderHeight = useContext(SetNavigationHeaderHeightContext)
@@ -119,7 +115,7 @@ export const NavigationBlurEffectHeader = ({
headerTransparent: true,
headerShown: true,
- headerLeft: headerLeft ?? (canBack ? () => : undefined),
+ headerLeft,
header: useTypeScriptHappyCallback(
({ options }) => (
@@ -147,15 +143,3 @@ export const NavigationBlurEffectHeader = ({
/>
)
}
-
-export const NavigationHeaderBackButton = () => {
- return
-}
-const NavigationHeaderBackButtonImpl = () => {
- const label = useColor("label")
- return (
- router.back()}>
-
-
- )
-}
diff --git a/apps/mobile/src/modules/entry-content/EntryTitle.tsx b/apps/mobile/src/modules/entry-content/EntryTitle.tsx
index 0083e5be8..2ef896ad5 100644
--- a/apps/mobile/src/modules/entry-content/EntryTitle.tsx
+++ b/apps/mobile/src/modules/entry-content/EntryTitle.tsx
@@ -11,14 +11,13 @@ import Animated, {
useSharedValue,
withTiming,
} from "react-native-reanimated"
-import { useColor } from "react-native-uikit-colors"
import { useUISettingKey } from "@/src/atoms/settings/ui"
+import { DefaultHeaderBackButton } from "@/src/components/layouts/header/NavigationHeader"
import { NavigationContext } from "@/src/components/layouts/views/NavigationContext"
import { NavigationBlurEffectHeader } from "@/src/components/layouts/views/SafeNavigationScrollView"
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
-import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line"
import { apiClient } from "@/src/lib/api-fetch"
import { EntryContentContext, useEntryContentContext } from "@/src/modules/entry-content/ctx"
import { EntryContentHeaderRightActions } from "@/src/modules/entry-content/EntryContentHeaderRightActions"
@@ -197,8 +196,6 @@ interface EntryLeftGroupProps {
}
const EntryLeftGroup = ({ canGoBack, entryId, titleOpacityShareValue }: EntryLeftGroupProps) => {
- const label = useColor("label")
-
const hideRecentReader = useUISettingKey("hideRecentReader")
const animatedOpacity = useAnimatedStyle(() => {
return {
@@ -208,7 +205,7 @@ const EntryLeftGroup = ({ canGoBack, entryId, titleOpacityShareValue }: EntryLef
return (
router.back()}>
- {canGoBack && }
+
{!hideRecentReader && (
diff --git a/apps/mobile/src/modules/settings/routes/Lists.tsx b/apps/mobile/src/modules/settings/routes/Lists.tsx
index cf7f6687d..e882c9221 100644
--- a/apps/mobile/src/modules/settings/routes/Lists.tsx
+++ b/apps/mobile/src/modules/settings/routes/Lists.tsx
@@ -1,11 +1,12 @@
import { router } from "expo-router"
import { createContext, createElement, useCallback, useContext, useMemo } from "react"
import type { ListRenderItem } from "react-native"
-import { ActivityIndicator, Image, StyleSheet, Text, TouchableOpacity, View } from "react-native"
+import { ActivityIndicator, Image, StyleSheet, Text, View } from "react-native"
import Animated, { LinearTransition } from "react-native-reanimated"
import { useColor } from "react-native-uikit-colors"
import { Balance } from "@/src/components/common/Balance"
+import { UINavigationHeaderActionButton } from "@/src/components/layouts/header/NavigationHeader"
import {
NavigationBlurEffectHeader,
SafeNavigationScrollView,
@@ -99,9 +100,9 @@ export const ListsScreen = () => {
const AddListButton = () => {
const labelColor = useColor("label")
return (
- router.push("/list")}>
+ router.push("/list")}>
-
+
)
}
diff --git a/apps/mobile/src/modules/settings/routes/ManageList.tsx b/apps/mobile/src/modules/settings/routes/ManageList.tsx
index 56e5f1353..f9e2638b5 100644
--- a/apps/mobile/src/modules/settings/routes/ManageList.tsx
+++ b/apps/mobile/src/modules/settings/routes/ManageList.tsx
@@ -6,6 +6,7 @@ import { createContext, useContext, useEffect, useMemo, useRef, useState } from
import { Text, View } from "react-native"
import { ModalHeaderSubmitButton } from "@/src/components/common/ModalSharedComponents"
+import { UINavigationHeaderActionButton } from "@/src/components/layouts/header/NavigationHeader"
import {
NavigationBlurEffectHeader,
SafeNavigationScrollView,
@@ -47,7 +48,10 @@ export const ManageListScreen = ({
const list = useList(id)
return (
-
+
{!!list && }
@@ -86,21 +90,23 @@ const ListImpl: React.FC<{ id: string }> = ({ id }) => {
(
- {
- addFeedsToFeedListMutation
- .mutateAsync()
- .then(() => {
- router.back()
- })
- .catch((error) => {
- toast.error(getBizFetchErrorMessage(error))
- console.error(error)
- })
- }}
- />
+
+ {
+ addFeedsToFeedListMutation
+ .mutateAsync()
+ .then(() => {
+ router.back()
+ })
+ .catch((error) => {
+ toast.error(getBizFetchErrorMessage(error))
+ console.error(error)
+ })
+ }}
+ />
+
)}
/>
diff --git a/apps/mobile/src/screens/(headless)/2fa.tsx b/apps/mobile/src/screens/(headless)/2fa.tsx
index 35ae93cc7..e2777527c 100644
--- a/apps/mobile/src/screens/(headless)/2fa.tsx
+++ b/apps/mobile/src/screens/(headless)/2fa.tsx
@@ -1,13 +1,7 @@
import { useMutation } from "@tanstack/react-query"
import { router } from "expo-router"
import { useMemo, useRef } from "react"
-import {
- Text,
- TouchableOpacity,
- TouchableWithoutFeedback,
- useAnimatedValue,
- View,
-} from "react-native"
+import { Text, TouchableWithoutFeedback, useAnimatedValue, View } from "react-native"
import { KeyboardController } from "react-native-keyboard-controller"
import type { OtpInputRef } from "react-native-otp-entry"
import { OtpInput } from "react-native-otp-entry"
@@ -15,7 +9,6 @@ import { useColor } from "react-native-uikit-colors"
import { NavigationContext } from "@/src/components/layouts/views/NavigationContext"
import { NavigationBlurEffectHeader } from "@/src/components/layouts/views/SafeNavigationScrollView"
-import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line"
import { twoFactor } from "@/src/lib/auth"
import { queryClient } from "@/src/lib/query-client"
import { toast } from "@/src/lib/toast"
@@ -55,18 +48,7 @@ export default function TwoFactorAuthScreen() {
return (
({ scrollY }), [scrollY])}>
- {
- return (
- router.back()}>
-
-
- )
- }}
- />
+
{
KeyboardController.dismiss()
diff --git a/apps/mobile/src/screens/(headless)/terms.tsx b/apps/mobile/src/screens/(headless)/terms.tsx
index d14f9eb39..56f93ad1c 100644
--- a/apps/mobile/src/screens/(headless)/terms.tsx
+++ b/apps/mobile/src/screens/(headless)/terms.tsx
@@ -1,9 +1,11 @@
-import { router, Stack, useNavigation } from "expo-router"
-import { TouchableOpacity, View } from "react-native"
-import { useColor } from "react-native-uikit-colors"
+import { getDefaultHeaderHeight } from "@react-navigation/elements"
+import { useMemo } from "react"
+import { useAnimatedValue, View } from "react-native"
+import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context"
+import { NavigationContext } from "@/src/components/layouts/views/NavigationContext"
+import { NavigationBlurEffectHeader } from "@/src/components/layouts/views/SafeNavigationScrollView"
import { Markdown } from "@/src/components/ui/typography/Markdown"
-import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line"
const txt = `# Terms of Service
@@ -93,26 +95,18 @@ export const TermsMarkdown = () => {
}
export default function Teams() {
- const canGoBack = useNavigation().canGoBack()
- const label = useColor("label")
+ const scrollY = useAnimatedValue(100)
+ const insets = useSafeAreaInsets()
+ const frame = useSafeAreaFrame()
+ const headerHeight = getDefaultHeaderHeight(frame, false, insets.top)
return (
-
- (
- router.back()}>
-
-
- )
- : undefined,
- }}
- />
+ ({ scrollY }), [scrollY])}>
+
+
-
-
+
+
+
+
)
}