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])}> + + - - + + + + ) }