From dfa5b0c0685a0c58ff0370db6480800661e2abd6 Mon Sep 17 00:00:00 2001 From: chenhang Date: Thu, 9 Jul 2026 15:18:52 +0800 Subject: [PATCH] refactor(chat): make external browser launch user initiated --- src/app/chat/chat-screen.tsx | 6 -- .../browser-hint-overlay.module.css | 44 +++++++++-- .../chat/components/browser-hint-overlay.tsx | 25 +++++-- .../chat/hooks/use-external-browser-prompt.ts | 75 ------------------- src/data/storage/app/app_storage.ts | 20 ----- src/data/storage/storage_keys.ts | 1 - src/lib/chat/chat_external_browser.ts | 21 +----- 7 files changed, 56 insertions(+), 136 deletions(-) delete mode 100644 src/app/chat/hooks/use-external-browser-prompt.ts diff --git a/src/app/chat/chat-screen.tsx b/src/app/chat/chat-screen.tsx index 8ccb4629..bde90076 100644 --- a/src/app/chat/chat-screen.tsx +++ b/src/app/chat/chat-screen.tsx @@ -34,7 +34,6 @@ import { useFirstRechargeOfferBanner } from "./hooks/use-first-recharge-offer-ba import { useChatUnlockNavigationFlow } from "./hooks/use-chat-unlock-navigation-flow"; import { useChatGuestLogin } from "./hooks/use-chat-guest-login"; import { useChatMessageLimitBanner } from "./hooks/use-chat-message-limit-banner"; -import { useExternalBrowserPrompt } from "./hooks/use-external-browser-prompt"; import styles from "./components/chat-screen.module.css"; export function ChatScreen() { @@ -91,11 +90,6 @@ export function ChatScreen() { loginStatus: authState.loginStatus, }); const shouldShowPwaInstall = state.historyLoaded && state.messages.length >= 10; - useExternalBrowserPrompt({ - hasInitialized: authState.hasInitialized, - isLoading: authState.isLoading, - loginStatus: authState.loginStatus, - }); useChatGuestLogin({ dispatch: authDispatch, diff --git a/src/app/chat/components/browser-hint-overlay.module.css b/src/app/chat/components/browser-hint-overlay.module.css index 0104318b..d178f412 100644 --- a/src/app/chat/components/browser-hint-overlay.module.css +++ b/src/app/chat/components/browser-hint-overlay.module.css @@ -9,22 +9,54 @@ align-items: flex-end; gap: var(--spacing-1, 4px); z-index: 20; - pointer-events: none; + max-width: min(52vw, 210px); + padding: 0; + border: 0; + background: transparent; + color: inherit; + cursor: pointer; + pointer-events: auto; + transition: opacity 0.16s ease, transform 0.16s ease; + -webkit-tap-highlight-color: transparent; } .emoji { font-size: var(--responsive-icon-size-md, 24px); line-height: 1; + filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.32)); } .hintBubble { padding: var(--spacing-2, 8px) var(--spacing-3, 12px); - background: var(--color-blur-background, rgba(13, 11, 20, 0.85)); - border-radius: var(--radius-md, 8px); + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: var(--radius-lg, 14px); + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.04)), + var(--color-blur-background, rgba(13, 11, 20, 0.84)); + box-shadow: + 0 10px 24px rgba(0, 0, 0, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.14); font-size: var(--font-size-sm, 12px); - font-weight: 500; + font-weight: 700; + line-height: 1.25; color: #fff; - white-space: pre-line; backdrop-filter: blur(10px); - max-width: min(52vw, 200px); + text-align: left; + white-space: pre-line; + overflow-wrap: break-word; +} + +.overlay:hover { + opacity: 0.94; + transform: translateY(-1px); +} + +.overlay:active { + opacity: 0.88; + transform: translateY(1px) scale(0.99); +} + +.overlay:focus-visible { + outline: 2px solid rgba(255, 255, 255, 0.88); + outline-offset: 4px; } diff --git a/src/app/chat/components/browser-hint-overlay.tsx b/src/app/chat/components/browser-hint-overlay.tsx index 27ab9663..44a57d6f 100644 --- a/src/app/chat/components/browser-hint-overlay.tsx +++ b/src/app/chat/components/browser-hint-overlay.tsx @@ -10,10 +10,11 @@ * 行为: * - 开发环境始终显示(测试方便) * - 生产环境仅在应用内浏览器中显示 - * - 视觉:右上角 👆 + 模糊背景气泡 + * - 点击后主动打开外部浏览器 */ import { useState } from "react"; +import { openChatInExternalBrowser } from "@/lib/chat/chat_external_browser"; import { BrowserDetector } from "@/utils"; import styles from "./browser-hint-overlay.module.css"; @@ -25,7 +26,7 @@ export interface BrowserHintOverlayProps { forceShow?: boolean; } -const DEFAULT_TEXT = "Tap ⋮ , open in browser\nfor better chat"; +const DEFAULT_TEXT = "Open in external browser\nto find me easily"; export function BrowserHintOverlay({ text = DEFAULT_TEXT, @@ -38,12 +39,20 @@ export function BrowserHintOverlay({ const shouldShow = forceShow || isInAppBrowser; if (!shouldShow) return null; + const handleClick = (): void => { + void openChatInExternalBrowser().catch((error) => { + console.warn("[chat] Failed to open external browser", error); + }); + }; + return ( -
- -
{text}
-
+ ); } diff --git a/src/app/chat/hooks/use-external-browser-prompt.ts b/src/app/chat/hooks/use-external-browser-prompt.ts deleted file mode 100644 index 3c6ab633..00000000 --- a/src/app/chat/hooks/use-external-browser-prompt.ts +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import { useEffect, useRef } from "react"; - -import { - openChatInExternalBrowser, - recordExternalBrowserPromptShown, - resolveExternalBrowserPromptEligibility, -} from "@/lib/chat/chat_external_browser"; - -import { - type ExternalBrowserPromptState, - shouldStartExternalBrowserPrompt, -} from "../chat-screen.helpers"; - -const EXTERNAL_BROWSER_PROMPT_DELAY_MS = 3000; - -export function useExternalBrowserPrompt({ - hasInitialized, - isLoading, - loginStatus, -}: ExternalBrowserPromptState): void { - const promptShownRef = useRef(false); - - useEffect(() => { - if ( - !shouldStartExternalBrowserPrompt({ - hasInitialized, - isLoading, - loginStatus, - }) - ) { - return; - } - - let mounted = true; - let timer: number | undefined; - - const init = async () => { - if (promptShownRef.current) return; - - const eligibility = await resolveExternalBrowserPromptEligibility(); - if (!mounted || !eligibility.canShow) return; - - promptShownRef.current = true; - timer = window.setTimeout(() => { - if (!mounted) return; - void (async () => { - try { - await recordExternalBrowserPromptShown(eligibility.today); - } catch (error) { - console.warn( - "[chat] Failed to record external browser auto launch", - error, - ); - } - if (!mounted) return; - try { - await openChatInExternalBrowser(); - } catch (error) { - console.warn("[chat] Failed to open external browser", error); - } - })(); - }, EXTERNAL_BROWSER_PROMPT_DELAY_MS); - }; - - void init(); - - return () => { - mounted = false; - if (timer !== undefined) window.clearTimeout(timer); - }; - }, [hasInitialized, isLoading, loginStatus]); - -} diff --git a/src/data/storage/app/app_storage.ts b/src/data/storage/app/app_storage.ts index 6916b036..d953e41d 100644 --- a/src/data/storage/app/app_storage.ts +++ b/src/data/storage/app/app_storage.ts @@ -41,26 +41,6 @@ export class AppStorage { return SpAsyncUtil.setBool(StorageKeys.pwaDialogShown, true); } - // ---- external browser dialog ---- - - static async canShowExternalBrowserDialog( - todayString: string, - ): Promise> { - return AppStorage.canReportKey( - StorageKeys.lastExternalBrowserDialogShown, - todayString, - ); - } - - static recordExternalBrowserDialogShown( - todayString: string, - ): Promise> { - return SpAsyncUtil.setString( - StorageKeys.lastExternalBrowserDialogShown, - todayString, - ); - } - // ---- PWA event reporting ---- static canReportPwaEvent(todayString: string): Promise> { diff --git a/src/data/storage/storage_keys.ts b/src/data/storage/storage_keys.ts index fbba197d..693640a0 100644 --- a/src/data/storage/storage_keys.ts +++ b/src/data/storage/storage_keys.ts @@ -29,7 +29,6 @@ export const StorageKeys = { // pwa / app info pwaDialogShown: "pwa_dialog_shown", - lastExternalBrowserDialogShown: "last_external_browser_dialog_shown", lastPwaEventReported: "last_pwa_event_reported", lastAppInfoReported: "last_app_info_reported", firstRechargeOfferBannerDismissed: "first_recharge_offer_banner_dismissed", diff --git a/src/lib/chat/chat_external_browser.ts b/src/lib/chat/chat_external_browser.ts index fe3c2d27..e5c3b0fd 100644 --- a/src/lib/chat/chat_external_browser.ts +++ b/src/lib/chat/chat_external_browser.ts @@ -1,28 +1,9 @@ "use client"; -import { AppStorage } from "@/data/storage/app/app_storage"; import { AuthStorage } from "@/data/storage/auth/auth_storage"; import { UserStorage } from "@/data/storage/user/user_storage"; import { ROUTES } from "@/router/routes"; -import { todayString, UrlLauncherUtil } from "@/utils"; - -export async function resolveExternalBrowserPromptEligibility(): Promise<{ - canShow: boolean; - today: string; -}> { - const today = todayString(); - const result = await AppStorage.canShowExternalBrowserDialog(today); - return { - canShow: result.success && result.data, - today, - }; -} - -export function recordExternalBrowserPromptShown( - today: string, -): Promise { - return AppStorage.recordExternalBrowserDialogShown(today); -} +import { UrlLauncherUtil } from "@/utils"; export async function openChatInExternalBrowser(): Promise { const authStorage = AuthStorage.getInstance();