From 46eada0c9fcde3d695a0bb783ea71cda0ef1586b Mon Sep 17 00:00:00 2001 From: Leon Date: Fri, 20 Oct 2023 15:30:48 +0800 Subject: [PATCH] feat: discover explorer --- package.json | 1 + .../components/DebugRenderTracker/index.tsx | 32 +++ .../kit/src/components/WebView/mock/index.ts | 10 + packages/kit/src/hooks/useIsMounted.ts | 14 ++ packages/kit/src/hooks/usePromiseResult.ts | 155 +++++++++++++ .../src/views/Components/stories/Webview.tsx | 28 ++- .../Explorer/Context/contextWebTabs.ts | 215 ++++++++++++++++++ .../Explorer/Desktop/ControllerBarDesktop.tsx | 143 ++++++++++++ .../Explorer/Desktop/ExplorerDesktop.tsx | 67 ++++++ .../Explorer/Desktop/TabBarDesktop.tsx | 176 ++++++++++++++ .../views/Discover/Explorer/explorerUtils.ts | 62 +++++ .../kit/src/views/Discover/Explorer/index.tsx | 22 ++ yarn.lock | 16 ++ 13 files changed, 930 insertions(+), 11 deletions(-) create mode 100644 packages/kit/src/components/DebugRenderTracker/index.tsx create mode 100644 packages/kit/src/hooks/useIsMounted.ts create mode 100644 packages/kit/src/hooks/usePromiseResult.ts create mode 100644 packages/kit/src/views/Discover/Explorer/Context/contextWebTabs.ts create mode 100644 packages/kit/src/views/Discover/Explorer/Desktop/ControllerBarDesktop.tsx create mode 100644 packages/kit/src/views/Discover/Explorer/Desktop/ExplorerDesktop.tsx create mode 100644 packages/kit/src/views/Discover/Explorer/Desktop/TabBarDesktop.tsx create mode 100644 packages/kit/src/views/Discover/Explorer/explorerUtils.ts create mode 100644 packages/kit/src/views/Discover/Explorer/index.tsx diff --git a/package.json b/package.json index 6c65c7df382..1df1e88dfb9 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint": "^8.25.0", "expo": "~49.0.13", "expo-status-bar": "~1.6.0", + "jotai": "^2.4.3", "memoizee": "^0.4.15", "natsort": "^2.0.3", "prettier": "^2.8.1", diff --git a/packages/kit/src/components/DebugRenderTracker/index.tsx b/packages/kit/src/components/DebugRenderTracker/index.tsx new file mode 100644 index 00000000000..8b927223a4a --- /dev/null +++ b/packages/kit/src/components/DebugRenderTracker/index.tsx @@ -0,0 +1,32 @@ +import { type ComponentType, type FC, type ReactElement, useRef } from 'react'; + +import platformEnv from '@onekeyhq/shared/src/platformEnv'; + +const css1 = 'debug-render-tracker-animated-bg'; +const css2 = 'debug-render-tracker-animated-bg0'; + +function DebugRenderTracker(props: { children: ReactElement }): ReactElement { + const { children } = props; + const cls = useRef(css1); + if (process.env.NODE_ENV !== 'production') { + if (platformEnv.isRuntimeBrowser) { + cls.current = cls.current === css1 ? css2 : css1; + return
{children}
; + } + } + + return children; +} + +const withDebugRenderTracker =

( + WrappedComponent: ComponentType

, +) => { + const WithRenderTracker: FC

= (props) => ( + + + + ); + + return WithRenderTracker; +}; +export { withDebugRenderTracker, DebugRenderTracker }; diff --git a/packages/kit/src/components/WebView/mock/index.ts b/packages/kit/src/components/WebView/mock/index.ts index 555bbeb7f4f..616fe0e2a93 100644 --- a/packages/kit/src/components/WebView/mock/index.ts +++ b/packages/kit/src/components/WebView/mock/index.ts @@ -4,3 +4,13 @@ export const backgroundApiProxy = { connectBridge: (jsBridge: any) => {}, bridgeReceiveHandler: (_: any): Promise => Promise.resolve({}), }; + +export const simpleDb = { + discoverWebTabs: { + getRawData: () => + Promise.resolve({ + tabs: [], + }), + setRawData: (_: any) => {}, + }, +}; diff --git a/packages/kit/src/hooks/useIsMounted.ts b/packages/kit/src/hooks/useIsMounted.ts new file mode 100644 index 00000000000..8bf20dd74a4 --- /dev/null +++ b/packages/kit/src/hooks/useIsMounted.ts @@ -0,0 +1,14 @@ +import { useEffect, useRef } from 'react'; + +function useIsMounted() { + const isMountedRef = useRef(true); + useEffect(() => { + isMountedRef.current = true; + return () => { + isMountedRef.current = false; + }; + }, []); + return isMountedRef; +} + +export { useIsMounted }; diff --git a/packages/kit/src/hooks/usePromiseResult.ts b/packages/kit/src/hooks/usePromiseResult.ts new file mode 100644 index 00000000000..9f95280ad62 --- /dev/null +++ b/packages/kit/src/hooks/usePromiseResult.ts @@ -0,0 +1,155 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; + +import { useIsFocused } from '@react-navigation/core'; +import { debounce } from 'lodash'; + +import { wait } from '../utils/helper'; + +import { useIsMounted } from './useIsMounted'; + +type IRunnerConfig = { triggerByDeps?: boolean }; + +type IPromiseResultOptions = { + initResult?: T; // TODO rename to initData + watchLoading?: boolean; // make isLoading work, which cause more once render + loadingDelay?: number; + checkIsMounted?: boolean; + checkIsFocused?: boolean; + debounced?: number; +}; + +export function usePromiseResult( + method: (...args: any[]) => Promise, + deps: any[], + options: { initResult: T } & IPromiseResultOptions, +): { result: T; isLoading: boolean | undefined }; + +export function usePromiseResult( + method: (...args: any[]) => Promise, + deps: any[], + options?: IPromiseResultOptions, +): { + result: T | undefined; + isLoading: boolean | undefined; + run: (config?: IRunnerConfig) => Promise; +}; + +export function usePromiseResult( + method: (...args: any[]) => Promise, + deps: any[] = [], + options: IPromiseResultOptions = {}, +): { + result: T | undefined; + isLoading: boolean | undefined; + run: (config?: IRunnerConfig) => Promise; +} { + const [result, setResult] = useState( + options.initResult as any, + ); + const [isLoading, setIsLoading] = useState(); + const isMountedRef = useIsMounted(); + const isFocused = useIsFocused(); + const isFocusedRef = useRef(isFocused); + isFocusedRef.current = isFocused; + const methodRef = useRef(method); + methodRef.current = method; + const optionsRef = useRef(options); + optionsRef.current = { + watchLoading: false, + loadingDelay: 0, + checkIsMounted: true, + checkIsFocused: true, + ...options, + }; + const isDepsChangedOnBlur = useRef(false); + + const run = useMemo( + () => { + const { watchLoading, loadingDelay, checkIsMounted, checkIsFocused } = + optionsRef.current; + + const setLoadingTrue = () => { + if (watchLoading) setIsLoading(true); + }; + const setLoadingFalse = () => { + if (watchLoading) setIsLoading(false); + }; + const shouldSetState = () => { + let flag = true; + if (checkIsMounted && !isMountedRef.current) { + flag = false; + } + if (checkIsFocused && !isFocusedRef.current) { + flag = false; + } + return flag; + }; + + const runner = async (config?: IRunnerConfig) => { + if (config?.triggerByDeps && !isFocusedRef.current) { + isDepsChangedOnBlur.current = true; + } + try { + if (shouldSetState()) { + setLoadingTrue(); + const r = await methodRef?.current?.(); + if (shouldSetState()) { + setResult(r); + } + } + } finally { + if (loadingDelay && watchLoading) { + await wait(loadingDelay); + } + if (shouldSetState()) { + setLoadingFalse(); + } + } + }; + + if (optionsRef.current.debounced) { + const runnderDebounced = debounce( + runner, + optionsRef.current.debounced, + { + leading: false, + trailing: true, + }, + ); + return async (config?: IRunnerConfig) => { + if (shouldSetState()) { + setLoadingTrue(); + } + await runnderDebounced(config); + }; + } + + return runner; + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + + useEffect(() => { + run({ triggerByDeps: true }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); + + useEffect(() => { + if ( + isFocused && + optionsRef.current.checkIsFocused && + isDepsChangedOnBlur.current + ) { + isDepsChangedOnBlur.current = false; + run(); + } + }, [isFocused, run]); + + // TODO rename result to data + return { result, isLoading, run }; +} + +export const useAsyncCall = usePromiseResult; +export const useAsyncResult = usePromiseResult; +export const useAsyncData = usePromiseResult; diff --git a/packages/kit/src/views/Components/stories/Webview.tsx b/packages/kit/src/views/Components/stories/Webview.tsx index cee32fe9d56..66bc0380094 100644 --- a/packages/kit/src/views/Components/stories/Webview.tsx +++ b/packages/kit/src/views/Components/stories/Webview.tsx @@ -1,13 +1,19 @@ -import WebView from '@onekeyhq/kit/src/components/WebView'; - -const WebviewGallery = () => ( - -); +// import WebView from '@onekeyhq/kit/src/components/WebView'; + +// const WebviewGallery = () => ( +// +// ); + +// export default WebviewGallery; + +import Explorer from '@onekeyhq/kit/src/views/Discover/Explorer'; + +const WebviewGallery = () => ; export default WebviewGallery; diff --git a/packages/kit/src/views/Discover/Explorer/Context/contextWebTabs.ts b/packages/kit/src/views/Discover/Explorer/Context/contextWebTabs.ts new file mode 100644 index 00000000000..931f0ec7ad4 --- /dev/null +++ b/packages/kit/src/views/Discover/Explorer/Context/contextWebTabs.ts @@ -0,0 +1,215 @@ +import { isEqual } from 'lodash'; +import { nanoid } from 'nanoid'; + +import { simpleDb } from '@onekeyhq/kit/src/components/WebView/mock'; +import { + atom, + createJotaiContext, +} from '@onekeyhq/kit/src/store/jotai/createJotaiContext'; + +import { webviewRefs } from '../explorerUtils'; + +export interface WebTab { + id: string; + url: string; + title?: string; + favicon?: string; + thumbnail?: string; + isCurrent: boolean; + isBookmarked?: boolean; + canGoBack?: boolean; + canGoForward?: boolean; + loading?: boolean; + refReady?: boolean; + timestamp?: number; +} + +export const homeTab: WebTab = { + id: 'home', + // current url in webview + url: 'about:blank', + title: 'OneKey', + isCurrent: true, + canGoBack: false, + loading: false, +}; + +export const homeResettingFlags: Record = {}; +// eslint-disable-next-line @typescript-eslint/naming-convention +let _currentTabId = ''; + +export const getCurrentTabId = () => + // TODO: Fix this + // if (!_currentTabId) { + // _currentTabId = webTabsObs.peek().find((t) => t.isCurrent)?.id || ''; + // } + _currentTabId; + +export function buildWebTabData(tabs: WebTab[]) { + const map: Record = {}; + const keys: string[] = []; + tabs.forEach((tab) => { + keys.push(tab.id); + map[tab.id] = tab; + }); + return { + data: tabs, + keys, + map, + }; +} + +interface IWebTabsAtom { + tabs: WebTab[]; + keys: string[]; +} + +export const atomWebTabs = atom({ tabs: [], keys: [] }); +export const atomWebTabsMap = atom>({ + [homeTab.id]: homeTab, +}); +export const setWebTabsWriteAtom = atom(null, (get, set, payload: WebTab[]) => { + let newTabs = payload; + if (!newTabs) { + newTabs = [{ ...homeTab }]; + } + const result = buildWebTabData(newTabs); + if (!isEqual(result.keys, get(atomWebTabs).keys)) { + console.log('===>refresh new data: ', result); + set(atomWebTabs, { keys: result.keys, tabs: result.data }); + } + + set(atomWebTabsMap, () => result.map); + simpleDb.discoverWebTabs.setRawData({ + tabs: newTabs, + }); +}); +export const addWebTabAtomWithWriteOnly = atom( + null, + (get, set, payload: Partial) => { + const { tabs } = get(atomWebTabs); + // TODO: Add limit for native + + if (!payload.id || payload.id === homeTab.id) { + payload.id = nanoid(); + } + + if (payload.isCurrent) { + for (const tab of tabs) { + tab.isCurrent = false; + } + _currentTabId = payload.id; + } + payload.timestamp = Date.now(); + set(setWebTabsWriteAtom, [...tabs, payload as WebTab]); + }, +); +export const addBlankWebTabAtomWithWriteOnly = atom(null, (_, set) => { + set(addWebTabAtomWithWriteOnly, { ...homeTab }); +}); +export const setWebTabDataAtomWithWriteOnly = atom( + null, + (get, set, payload: Partial) => { + const { tabs } = get(atomWebTabs); + const tabIndex = tabs.findIndex((t) => t.id === payload.id); + if (tabIndex > -1) { + const tabToModify = tabs[tabIndex]; + Object.keys(payload).forEach((k) => { + const key = k as keyof WebTab; + const value = payload[key]; + if (value !== undefined && value !== tabToModify[key]) { + if (key === 'title' && !value) { + return; + } + // @ts-expect-error + tabToModify[key] = value; + if (key === 'url') { + tabToModify.timestamp = Date.now(); + if (value === homeTab.url && payload.id) { + homeResettingFlags[payload.id] = tabToModify.timestamp; + } + if (!payload.favicon) { + try { + tabToModify.favicon = `${ + new URL(tabToModify.url ?? '').origin + }/favicon.ico`; + } catch { + // ignore + } + } + } + } + }); + tabs[tabIndex] = tabToModify; + set(setWebTabsWriteAtom, tabs); + } + }, +); +export const closeWebTabAtomWithWriteOnly = atom( + null, + (get, set, tabId: string) => { + delete webviewRefs[tabId]; + const { tabs } = get(atomWebTabs); + const targetIndex = tabs.findIndex((t) => t.id === tabId); + if (targetIndex !== -1) { + if (tabs[targetIndex].isCurrent) { + const prev = tabs[targetIndex - 1]; + if (prev) { + prev.isCurrent = true; + _currentTabId = prev.id; + } + } + tabs.splice(targetIndex, 1); + set(setWebTabsWriteAtom, [...tabs]); + } + }, +); +export const closeAllWebTabsAtomWithWriteOnly = atom(null, (_, set) => { + for (const id of Object.getOwnPropertyNames(webviewRefs)) { + delete webviewRefs[id]; + } + set(setWebTabsWriteAtom, [{ ...homeTab }]); + _currentTabId = homeTab.id; +}); +export const setCurrentWebTabAtomWithWriteOnly = atom( + null, + (get, set, tabId: string) => { + const currentTabId = getCurrentTabId(); + if (currentTabId !== tabId) { + // pauseDappInteraction(currentTabId); + const { tabs } = get(atomWebTabs); + let previousTabUpdated = false; + let nextTabUpdated = false; + + for (let i = 0; i < tabs.length; i += 1) { + const tab = tabs[i]; + if (tab.isCurrent) { + tabs[i] = { + ...tab, + isCurrent: false, + }; + previousTabUpdated = true; + } else if (tab.id === tabId) { + tabs[i] = { + ...tab, + isCurrent: true, + }; + nextTabUpdated = true; + } + if (previousTabUpdated && nextTabUpdated) { + break; + } + } + set(setWebTabsWriteAtom, tabs); + // resumeDappInteraction(tabId); + _currentTabId = tabId; + } + }, +); + +export const incomingUrlAtom = atom(''); + +const { withProvider: withProviderWebTabs, useContextAtom: useAtomWebTabs } = + createJotaiContext(); + +export { withProviderWebTabs, useAtomWebTabs }; diff --git a/packages/kit/src/views/Discover/Explorer/Desktop/ControllerBarDesktop.tsx b/packages/kit/src/views/Discover/Explorer/Desktop/ControllerBarDesktop.tsx new file mode 100644 index 00000000000..388129e3cda --- /dev/null +++ b/packages/kit/src/views/Discover/Explorer/Desktop/ControllerBarDesktop.tsx @@ -0,0 +1,143 @@ +import type { ComponentProps } from 'react'; +import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; + +import { useIntl } from 'react-intl'; + +import { IconButton, Input, XStack } from '@onekeyhq/components'; +import type { ICON_NAMES } from '@onekeyhq/components'; + +import type { WebTab } from '../Context/contextWebTabs'; +import type { TextInput } from 'react-native'; + +type BrowserURLInputProps = { + onClear?: () => void; + onChangeText?: (text: string) => void; + customLeftIcon?: ICON_NAMES; +} & Omit, 'onChange' | 'onChangeText'>; + +const BrowserURLInput = forwardRef( + ({ value, onClear, onChangeText, customLeftIcon, ...props }, ref) => { + const [innerValue, setInnerValue] = useState(value); + const handleChangeText = useCallback( + (text: string) => { + if (typeof value === 'undefined') { + setInnerValue(text); + } else if (typeof onChangeText !== 'undefined') { + onChangeText(text); + } + }, + [value, onChangeText], + ); + + return ( + + ); + }, +); +BrowserURLInput.displayName = 'BrowserURLInput'; + +function getHttpSafeState(searchContent?: string): ICON_NAMES { + try { + if (!searchContent) { + return 'SearchOutline'; + } + + const url = new URL(searchContent); + if (url.protocol === 'https:') { + return 'CheckRadioOutline'; + } + if (url.protocol === 'http:') { + return 'BrokenLinkOutline'; + } + } catch (e) { + return 'SearchOutline'; + } + return 'SearchOutline'; +} + +function ControllerBarDesktop() { + const intl = useIntl(); + + const [historyVisible, setHistoryVisible] = useState(false); + const currentTab: WebTab = {} as WebTab; + + const url: string = + currentTab?.url && currentTab?.url !== 'about:blank' ? currentTab.url : ''; + const httpSafeState = getHttpSafeState(url); + const [searchText, setSearchText] = useState(url); + + useEffect(() => { + setSearchText(url); + }, [url]); + + const searchBar = useRef(null); + + return ( + + console.log('goBack')} + /> + console.log('Go Forward')} + /> + console.log('Reload')} /> + + setSearchText('')} + onSubmitEditing={({ nativeEvent: { text } }) => { + const trimText = text.trim(); + if (trimText) { + setSearchText(trimText); + // onSearchSubmitEditing(trimText); + } + }} + onKeyPress={(event) => { + const { key } = event.nativeEvent; + if (key === 'ArrowUp' || key === 'ArrowDown' || key === 'Enter') { + // if (onKeyEvent?.(key)) { + // // 阻断 上键、下键 事件传递 + // event.preventDefault(); + // } + } + }} + selectTextOnFocus + onFocus={() => { + setHistoryVisible(true); + }} + onBlur={() => { + if (searchText?.trim() === '') { + setSearchText(url); + } + setHistoryVisible(false); + }} + /> + + {/* */} + {/* BrowserToolbar gas pannel */} + {/* Toolbar More Menu */} + + // Add: SearchView + ); +} + +export default ControllerBarDesktop; diff --git a/packages/kit/src/views/Discover/Explorer/Desktop/ExplorerDesktop.tsx b/packages/kit/src/views/Discover/Explorer/Desktop/ExplorerDesktop.tsx new file mode 100644 index 00000000000..5840d6d3172 --- /dev/null +++ b/packages/kit/src/views/Discover/Explorer/Desktop/ExplorerDesktop.tsx @@ -0,0 +1,67 @@ +import { useEffect } from 'react'; + +import { Stack, useThemeValue } from '@onekeyhq/components'; +import useSafeAreaInsets from '@onekeyhq/components/src/Provider/hooks/useSafeAreaInsets'; +import { simpleDb } from '@onekeyhq/kit/src/components/WebView/mock'; + +import { usePromiseResult } from '../../../../hooks/usePromiseResult'; +import { + homeTab, + setWebTabsWriteAtom, + useAtomWebTabs, + withProviderWebTabs, +} from '../Context/contextWebTabs'; +import { webHandler } from '../explorerUtils'; + +// import ControllerBarDesktop from './ControllerBarDesktop'; +import TabBarDesktop from './TabBarDesktop'; + +const showExplorerBar = webHandler !== 'browser'; + +export function useTabBarDataFromSimpleDb() { + const result = usePromiseResult(async () => { + const r = await simpleDb.discoverWebTabs.getRawData(); + return r?.tabs || [{ ...homeTab }]; + }, []); + + return result; +} + +function HandleRebuildTabBarData() { + const result = useTabBarDataFromSimpleDb(); + const [, setWebTabsData] = useAtomWebTabs(setWebTabsWriteAtom); + useEffect(() => { + const data = result.result; + console.log('===>result: ', data); + if (data && Array.isArray(data)) { + setWebTabsData(data); + } + }, [result.result, setWebTabsData]); + + return null; +} + +function ExplorerHeaderCmp() { + const { top } = useSafeAreaInsets(); + const tabBarBgColor = useThemeValue('bgSubdued') as string; + return ( + + + + {/* */} + + ); +} + +const ExplorerHeader = withProviderWebTabs(ExplorerHeaderCmp); + +function ExplorerDesktop() { + return ( + + {!showExplorerBar && } + WebView Content + + ); +} + +export default ExplorerDesktop; diff --git a/packages/kit/src/views/Discover/Explorer/Desktop/TabBarDesktop.tsx b/packages/kit/src/views/Discover/Explorer/Desktop/TabBarDesktop.tsx new file mode 100644 index 00000000000..4898dc7e102 --- /dev/null +++ b/packages/kit/src/views/Discover/Explorer/Desktop/TabBarDesktop.tsx @@ -0,0 +1,176 @@ +import { memo, useCallback, useMemo } from 'react'; + +import { Image } from 'react-native'; + +import { IconButton, Stack, Text } from '@onekeyhq/components'; +// @ts-expect-error +import dAppFavicon from '@onekeyhq/kit/assets/dapp_favicon.png'; +import { DebugRenderTracker } from '@onekeyhq/kit/src/components/DebugRenderTracker'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; + +import { + addBlankWebTabAtomWithWriteOnly, + atomWebTabs, + atomWebTabsMap, + closeWebTabAtomWithWriteOnly, + setCurrentWebTabAtomWithWriteOnly, + useAtomWebTabs, +} from '../Context/contextWebTabs'; +import { dismissWebviewKeyboard } from '../explorerUtils'; + +import type { WebTab } from '../Context/contextWebTabs'; +import type { LayoutChangeEvent } from 'react-native'; + +function useTabActions({ id }: { id: string }) { + const [, setCurrentWebTabAction] = useAtomWebTabs( + setCurrentWebTabAtomWithWriteOnly, + ); + const setCurrentTab = useCallback(() => { + if (platformEnv.isNative) { + dismissWebviewKeyboard(); + } + setCurrentWebTabAction(id); + }, [id, setCurrentWebTabAction]); + + const [, closeWebTab] = useAtomWebTabs(closeWebTabAtomWithWriteOnly); + const closeTab = useCallback(() => { + closeWebTab(id); + }, [id, closeWebTab]); + + return { + setCurrentTab, + closeTab, + }; +} + +function HomeTab({ id }: WebTab) { + const [map] = useAtomWebTabs(atomWebTabsMap); + const tab = map[id || '']; + const { setCurrentTab } = useTabActions({ id: tab.id }); + const ButtonContent = useMemo( + () => ( + + + + ), + [tab.isCurrent, setCurrentTab], + ); + return <>{ButtonContent}; +} + +function StandardTab({ + id, + onLayout, +}: WebTab & { onLayout?: (e: LayoutChangeEvent) => void }) { + const [map] = useAtomWebTabs(atomWebTabsMap); + const tab = map[id || '']; + const { setCurrentTab, closeTab } = useTabActions({ id: tab.id }); + + const Content = useMemo( + () => ( + + + + + {tab?.title} + + { + e.stopPropagation(); + closeTab(); + }} + /> + + + ), + [tab.isCurrent, tab.title, tab.favicon, setCurrentTab, closeTab, onLayout], + ); + + return <>{Content}; +} + +function SelectedTabCmp({ + tab, + onLayout, +}: { + tab: WebTab; + onLayout?: (e: LayoutChangeEvent) => void; +}) { + if (tab.id === 'home') { + return ; + } + return ; +} + +const TabWithMemo = memo(SelectedTabCmp); + +function Tab({ + tab, + onLayout, +}: { + tab: WebTab; + onLayout?: (e: LayoutChangeEvent) => void; +}) { + return ( + + + + ); +} + +const AddTabButton = () => { + const [, addBlankWebTab] = useAtomWebTabs(addBlankWebTabAtomWithWriteOnly); + return ( + addBlankWebTab()} + icon="PlusSmallOutline" + /> + ); +}; + +function TabBarDesktop() { + const [webTabs] = useAtomWebTabs(atomWebTabs); + console.log('tabs ===>: ', webTabs); + return ( + + {webTabs.tabs?.map((tab) => ( + + ))} + + + ); +} + +export default TabBarDesktop; diff --git a/packages/kit/src/views/Discover/Explorer/explorerUtils.ts b/packages/kit/src/views/Discover/Explorer/explorerUtils.ts new file mode 100644 index 00000000000..b3bc7440e1a --- /dev/null +++ b/packages/kit/src/views/Discover/Explorer/explorerUtils.ts @@ -0,0 +1,62 @@ +import platformEnv from '@onekeyhq/shared/src/platformEnv'; + +import type { IElectronWebView } from '@onekeyfe/cross-inpage-provider-types'; +import type { IWebViewWrapperRef } from '@onekeyfe/onekey-cross-webview'; +import type { WebView } from 'react-native-webview'; + +export const webviewRefs: Record = {}; + +export type WebHandler = 'browser' | 'tabbedWebview'; +export const webHandler: WebHandler = (() => { + if (platformEnv.isDesktop || platformEnv.isNative) { + return 'tabbedWebview'; + } + return 'browser'; +})(); + +export function getWebviewWrapperRef(id?: string) { + let tabId = id; + if (!tabId) { + const { getCurrentTabId } = + require('./Context/contextWebTabs') as typeof import('./Context/contextWebTabs'); + tabId = getCurrentTabId(); + } + const ref = tabId ? webviewRefs[tabId] : null; + return ref ?? null; +} + +// for hide keyboard +const injectToDismissWebviewKeyboard = ` +(function(){ + document.activeElement && document.activeElement.blur() +})() +`; + +export function dismissWebviewKeyboard(id?: string) { + const ref = getWebviewWrapperRef(id); + if (ref) { + if (platformEnv.isNative) { + try { + (ref.innerRef as WebView)?.injectJavaScript( + injectToDismissWebviewKeyboard, + ); + } catch (error) { + // ipad mini orientation changed cause injectJavaScript ERROR, which crash app + console.error( + 'blurActiveElement webview.injectJavaScript() ERROR >>>>> ', + error, + ); + } + } + if (platformEnv.isDesktop) { + const deskTopRef = ref.innerRef as IElectronWebView; + if (deskTopRef) { + try { + deskTopRef.executeJavaScript(injectToDismissWebviewKeyboard); + } catch (e) { + // if not dom ready, no need to pause websocket + } + } + } + } +} diff --git a/packages/kit/src/views/Discover/Explorer/index.tsx b/packages/kit/src/views/Discover/Explorer/index.tsx new file mode 100644 index 00000000000..76f75a766ec --- /dev/null +++ b/packages/kit/src/views/Discover/Explorer/index.tsx @@ -0,0 +1,22 @@ +import { memo } from 'react'; + +import { Stack } from '@onekeyhq/components'; +import useIsVerticalLayout from '@onekeyhq/components/src/Provider/hooks/useIsVerticalLayout'; +import platformEnv from '@onekeyhq/shared/src/platformEnv'; + +let ExplorerDesktop: any; + +function Explorer() { + const isVerticalLayout = useIsVerticalLayout(); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + ExplorerDesktop = require('./Desktop/ExplorerDesktop').default; + + return ( + + {isVerticalLayout ? : } + + ); +} + +export default memo(Explorer); diff --git a/yarn.lock b/yarn.lock index 483523bfa71..43f06511bd1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4684,6 +4684,7 @@ __metadata: jest: ^29.7.0 jest-expo: ^49.0.0 jest-html-reporter: ^3.10.2 + jotai: ^2.4.3 memoizee: ^0.4.15 natsort: ^2.0.3 node-notifier: ^10.0.1 @@ -18419,6 +18420,21 @@ __metadata: languageName: node linkType: hard +"jotai@npm:^2.4.3": + version: 2.4.3 + resolution: "jotai@npm:2.4.3" + peerDependencies: + "@types/react": ">=17.0.0" + react: ">=17.0.0" + peerDependenciesMeta: + "@types/react": + optional: true + react: + optional: true + checksum: 4991faba3284978bda3d90c6b1fc711759b76ce83ff04edb181d7929951ee54cb077793e5e4b0bba2ddf07fb6681a97a9d3007e2697d6ef181a66479ef9b2116 + languageName: node + linkType: hard + "js-base64@npm:3.7.5": version: 3.7.5 resolution: "js-base64@npm:3.7.5"