From ad8a08a02843d6e476272625dab13243195de27c Mon Sep 17 00:00:00 2001 From: Christian Baroni Date: Thu, 4 Apr 2024 17:32:30 -0400 Subject: [PATCH 1/3] Browser: tab transitions, state update queue (#5582) * Tab transitions, add tab state update queue * Move unblocking to the end of setTabStatesThenUnblockQueue, remove some commented code * Remove commented code, adjust newTab checks * Make condition more readable Same logic but less confusing * Fix background color conversion * Remove redundant multipleTabsOpen logic, make conditions more legible * Fade in tabs on mount, clean up WebViewBorder * Fix part of the scroll jank * Queue logic cleanup, add some error logging TODOs * Catch up with develop * Remove unnecessary runOnJS * More performant fade in on mount * Lower fade duration * Lint --------- Co-authored-by: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> --- src/components/DappBrowser/BrowserContext.tsx | 308 ++++++++++++++---- src/components/DappBrowser/BrowserTab.tsx | 178 +++++++--- src/components/DappBrowser/CloseTabButton.tsx | 164 +++++++--- src/components/DappBrowser/TabViewToolbar.tsx | 14 +- src/components/DappBrowser/WebViewBorder.tsx | 8 +- src/components/DappBrowser/utils.ts | 7 + 6 files changed, 520 insertions(+), 159 deletions(-) diff --git a/src/components/DappBrowser/BrowserContext.tsx b/src/components/DappBrowser/BrowserContext.tsx index 73ed0be8068..50545c3a208 100644 --- a/src/components/DappBrowser/BrowserContext.tsx +++ b/src/components/DappBrowser/BrowserContext.tsx @@ -8,6 +8,7 @@ import Animated, { SharedValue, runOnJS, runOnUI, + useAnimatedReaction, useAnimatedRef, useScrollViewOffset, useSharedValue, @@ -15,7 +16,7 @@ import Animated, { } from 'react-native-reanimated'; import WebView from 'react-native-webview'; import { SPRING_CONFIGS } from '@/components/animations/animationConfigs'; -import { generateUniqueId } from './utils'; +import { generateUniqueId, generateUniqueIdWorklet } from './utils'; interface BrowserTabViewProgressContextType { tabViewProgress: SharedValue | undefined; @@ -39,11 +40,12 @@ interface BrowserContextType { activeTabIndex: number; activeTabRef: React.MutableRefObject; animatedActiveTabIndex: SharedValue | undefined; - closeTab: (tabId: string) => void; + closeTabWorklet: (tabId: string, tabIndex: number) => void; + currentlyOpenTabIds: SharedValue | undefined; goBack: () => void; goForward: () => void; loadProgress: SharedValue | undefined; - newTab: () => void; + newTabWorklet: () => void; onRefresh: () => void; searchInputRef: React.RefObject; searchViewProgress: SharedValue | undefined; @@ -65,6 +67,14 @@ export interface TabState { logoUrl?: string | null; } +type TabOperationType = 'newTab' | 'closeTab'; + +interface TabOperation { + type: TabOperationType; + tabId: string; + newActiveIndex: number | undefined; +} + export const RAINBOW_HOME = 'RAINBOW_HOME'; const DEFAULT_TAB_STATE: TabState[] = [ @@ -83,16 +93,17 @@ const DEFAULT_BROWSER_CONTEXT: BrowserContextType = { activeTabIndex: 0, activeTabRef: { current: null }, animatedActiveTabIndex: undefined, - closeTab: () => { + closeTabWorklet: () => { return; }, + currentlyOpenTabIds: undefined, goBack: () => { return; }, goForward: () => { return; }, - newTab: () => { + newTabWorklet: () => { return; }, onRefresh: () => { @@ -128,8 +139,6 @@ export const useBrowserContext = () => useContext(BrowserContext); const tabStateStore = new MMKV(); -const EMPTY_TAB_STATE: TabState[] = []; - export const BrowserContextProvider = ({ children }: { children: React.ReactNode }) => { const [activeTabIndex, setActiveTabIndex] = useState(0); const [tabStates, setTabStates] = useMMKVObject('tabStateStorage', tabStateStore); @@ -138,7 +147,7 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode (newState: Partial, tabId?: string) => { if (!tabStates) return; - const tabIndex = tabId ? tabStates.findIndex(tab => tab.uniqueId === tabId) : activeTabIndex; + const tabIndex = tabId ? tabStates?.findIndex(tab => tab.uniqueId === tabId) : activeTabIndex; if (tabIndex === -1) return; if (isEqual(tabStates[tabIndex], newState)) return; @@ -162,16 +171,40 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode const animatedActiveTabIndex = useSharedValue(0); const { tabViewProgress } = useBrowserTabViewProgressContext(); + // We use the currentlyOpenTabIds shared value as an always-up-to-date source of truth for which + // tabs are open at any given moment, inclusive of any pending tab operations. This ensures that + // a stale version of tabStates is never used when multiple tabs are closed or created quickly. + // This value is updated in real time when a 'tabClose' or 'newTab' operation initiates. + const currentlyOpenTabIds = useSharedValue(tabStates?.map(tab => tab.uniqueId) || []); + + const tabOperationQueue = useSharedValue([]); + const shouldBlockOperationQueue = useSharedValue(false); + const toggleTabViewWorklet = useCallback( (activeIndex?: number) => { 'worklet'; const willTabViewBecomeVisible = !tabViewVisible.value; const tabIndexProvided = activeIndex !== undefined; - if (tabIndexProvided && !willTabViewBecomeVisible) { + if (!willTabViewBecomeVisible && tabIndexProvided) { animatedActiveTabIndex.value = activeIndex; runOnJS(setActiveTabIndex)(activeIndex); + } else if (!willTabViewBecomeVisible && animatedActiveTabIndex.value < 0) { + // If the index is negative here, it indicates that a previously active tab was closed, + // and the tab view was then exited via the "Done" button. We can identify the correct + // tab to make active now by flipping the current index back to a positive number. + // Note: The code that sets this negative index can be found in closeTabWorklet(). + const indexToMakeActive = Math.abs(animatedActiveTabIndex.value); + + if (indexToMakeActive < currentlyOpenTabIds.value.length) { + animatedActiveTabIndex.value = indexToMakeActive; + runOnJS(setActiveTabIndex)(indexToMakeActive); + } else { + animatedActiveTabIndex.value = currentlyOpenTabIds.value.length - 1; + runOnJS(setActiveTabIndex)(currentlyOpenTabIds.value.length - 1); + } } + if (tabViewProgress !== undefined) { tabViewProgress.value = willTabViewBecomeVisible ? withSpring(100, SPRING_CONFIGS.browserTabTransition) @@ -180,62 +213,220 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode tabViewVisible.value = willTabViewBecomeVisible; }, - [animatedActiveTabIndex, tabViewProgress, tabViewVisible] + [animatedActiveTabIndex, currentlyOpenTabIds, tabViewProgress, tabViewVisible] ); - const newTab = useCallback(() => { - const newTabToAdd = { - canGoBack: false, - canGoForward: false, - uniqueId: generateUniqueId(), - url: RAINBOW_HOME, - }; - - if (!tabStates) { - setTabStates([newTabToAdd]); - runOnUI(toggleTabViewWorklet)(0); - } else { - const updatedTabs = [...tabStates, newTabToAdd]; - setTabStates(updatedTabs); - runOnUI(toggleTabViewWorklet)(updatedTabs.length - 1); - } - }, [setTabStates, tabStates, toggleTabViewWorklet]); + const requestTabOperationsWorklet = useCallback( + (operations: TabOperation | TabOperation[]) => { + 'worklet'; + if (Array.isArray(operations)) { + tabOperationQueue.modify(currentQueue => { + currentQueue.push(...operations); + return currentQueue; + }); + } else { + tabOperationQueue.modify(currentQueue => { + currentQueue.push(operations); + return currentQueue; + }); + } + }, + [tabOperationQueue] + ); - const closeTab = useCallback( - (tabId: string) => { - if (!tabStates) return; + const setTabStatesThenUnblockQueue = useCallback( + (updatedTabStates: TabState[], shouldToggleTabView?: boolean, indexToMakeActive?: number) => { + setTabStates(updatedTabStates); - const tabIndex = tabStates.findIndex(tab => tab.uniqueId === tabId); - if (tabIndex === -1) return; + if (shouldToggleTabView) { + runOnUI(toggleTabViewWorklet)(indexToMakeActive); + } else if (indexToMakeActive !== undefined) { + setActiveTabIndex(indexToMakeActive); + } - const isActiveTab = tabIndex === activeTabIndex; - const isLastTab = tabIndex === tabStates.length - 1; - const hasNextTab = tabIndex < tabStates.length - 1; - - let newActiveTabIndex = activeTabIndex; - - if (isActiveTab) { - if (isLastTab && tabIndex === 0) { - setActiveTabIndex(0); - animatedActiveTabIndex.value = 0; - setTabStates(EMPTY_TAB_STATE); - newTab(); - return; - } else if (isLastTab && tabIndex > 0) { - newActiveTabIndex = tabIndex - 1; - } else if (hasNextTab) { - newActiveTabIndex = tabIndex; + shouldBlockOperationQueue.value = false; + }, + [setTabStates, shouldBlockOperationQueue, toggleTabViewWorklet] + ); + + const processOperationQueueWorklet = useCallback(() => { + 'worklet'; + if (shouldBlockOperationQueue.value || tabOperationQueue.value.length === 0) { + return; + } + + shouldBlockOperationQueue.value = true; + + let shouldToggleTabView = false; + let newActiveIndex: number | undefined = animatedActiveTabIndex.value; + + tabOperationQueue.modify(currentQueue => { + const newTabStates = tabStates || []; + // Process closeTab operations from oldest to newest + for (let i = 0; i < currentQueue.length; i++) { + const operation = currentQueue[i]; + if (operation.type === 'closeTab') { + const indexToClose = newTabStates.findIndex(tab => tab.uniqueId === operation.tabId); + if (indexToClose !== -1) { + newTabStates.splice(indexToClose, 1); + // Check to ensure we are setting a valid active tab index + if (operation.newActiveIndex === undefined) { + newActiveIndex = undefined; + } else { + const requestedNewActiveIndex = Math.abs(operation.newActiveIndex); + const isRequestedIndexValid = requestedNewActiveIndex >= 0 && requestedNewActiveIndex < currentlyOpenTabIds.value.length; + if (isRequestedIndexValid) { + newActiveIndex = operation.newActiveIndex; + } else { + // Make the last tab active if the requested index is not found + // (Negative to avoid immediately making the tab active - see notes in closeTabWorklet()) + newActiveIndex = -(currentlyOpenTabIds.value.length - 1); + } + } + } else { + // ⚠️ TODO: Add logging here to report any time a tab close operation was registered for a + // nonexistent tab (should never happen) + } + // Remove the operation from the queue after processing + currentQueue.splice(i, 1); + } + } + // Then process newTab operations from oldest to newest + for (let i = 0; i < currentQueue.length; i++) { + const operation = currentQueue[i]; + if (operation.type === 'newTab') { + // Check to ensure the tabId exists in currentlyOpenTabIds before creating the tab + const indexForNewTab = currentlyOpenTabIds.value.findIndex(tabId => tabId === operation.tabId); + if (indexForNewTab !== -1) { + const newTab = { + canGoBack: false, + canGoForward: false, + uniqueId: operation.tabId, + url: RAINBOW_HOME, + }; + newTabStates.push(newTab); + shouldToggleTabView = true; + newActiveIndex = indexForNewTab; + } else { + // ⚠️ TODO: Add logging here to report any time a new tab operation is given a nonexistent + // tabId (should never happen) + } + // Remove the operation from the queue after processing + currentQueue.splice(i, 1); } - } else if (tabIndex < activeTabIndex) { - newActiveTabIndex = activeTabIndex - 1; } - const updatedTabs = [...tabStates.slice(0, tabIndex), ...tabStates.slice(tabIndex + 1)]; - setTabStates(updatedTabs); - setActiveTabIndex(newActiveTabIndex); - animatedActiveTabIndex.value = newActiveTabIndex; + if (newActiveIndex !== undefined && (tabViewVisible.value || newActiveIndex >= 0)) { + animatedActiveTabIndex.value = newActiveIndex; + } else { + const currentActiveIndex = tabViewVisible?.value ? Math.abs(animatedActiveTabIndex.value) : animatedActiveTabIndex.value; + const isCurrentIndexValid = currentActiveIndex >= 0 && currentActiveIndex < currentlyOpenTabIds.value.length; + const indexToSet = isCurrentIndexValid ? animatedActiveTabIndex.value : currentlyOpenTabIds.value.length - 1; + newActiveIndex = indexToSet; + animatedActiveTabIndex.value = indexToSet; + } + + runOnJS(setTabStatesThenUnblockQueue)( + newTabStates, + shouldToggleTabView, + // If a new tab was created, the new tab will be the last tab and it should be made active now. + // We've already set the animatedActiveTabIndex to the correct index above, but the JS-side + // activeTabIndex still needs to be set, so we pass it along to setTabStatesThenUnblockQueue(). + newActiveIndex + ); + + // Return the remaining queue after processing, which should be empty + return currentQueue; + }); + }, [ + animatedActiveTabIndex, + currentlyOpenTabIds, + setTabStatesThenUnblockQueue, + shouldBlockOperationQueue, + tabOperationQueue, + tabStates, + tabViewVisible, + ]); + + const newTabWorklet = useCallback(() => { + 'worklet'; + const tabIdsInStates = new Set(tabStates?.map(state => state.uniqueId)); + const isNewTabOperationPending = + tabOperationQueue.value.some(operation => operation.type === 'newTab') || + currentlyOpenTabIds.value.some(tabId => !tabIdsInStates.has(tabId)); + + // The first check is mainly to guard against an edge case that happens when the new tab button is + // pressed just after the last tab is closed, but before a new blank tab has opened programatically, + // which results in two tabs being created when the user most certainly only wanted one. + if (!isNewTabOperationPending && (tabViewVisible.value || currentlyOpenTabIds.value.length === 0)) { + const tabIdForNewTab = generateUniqueIdWorklet(); + const newActiveIndex = currentlyOpenTabIds.value.length - 1; + + currentlyOpenTabIds.modify(value => { + value.push(tabIdForNewTab); + return value; + }); + requestTabOperationsWorklet({ type: 'newTab', tabId: tabIdForNewTab, newActiveIndex }); + } + }, [currentlyOpenTabIds, requestTabOperationsWorklet, tabOperationQueue, tabStates, tabViewVisible]); + + const closeTabWorklet = useCallback( + (tabId: string, tabIndex: number) => { + 'worklet'; + + // Note: The closed tab is removed from currentlyOpenTabIds ahead of time in BrowserTab as soon + // as the tab is swiped away, so that any operations applied between the time the swipe gesture + // is released and the time the tab is actually closed are aware of the pending deletion of the + // tab. The logic below assumes that the tab has already been removed from currentlyOpenTabIds. + + const currentActiveIndex = Math.abs(animatedActiveTabIndex.value); + const isActiveTab = tabIndex === currentActiveIndex; + const isLastRemainingTab = currentlyOpenTabIds.value.length === 0; + // ⬆️ These two ⬇️ checks account for the tab already being removed from currentlyOpenTabIds + const tabExistsAtNextIndex = tabIndex < currentlyOpenTabIds.value.length; + + let newActiveIndex: number | undefined = currentActiveIndex; + + if (isLastRemainingTab) { + requestTabOperationsWorklet({ type: 'closeTab', tabId, newActiveIndex: 0 }); + newTabWorklet(); + return; + } else if (isActiveTab) { + newActiveIndex = tabExistsAtNextIndex ? tabIndex : tabIndex - 1; + } else if (tabIndex < currentActiveIndex) { + newActiveIndex = currentActiveIndex - 1; + } + + const tabIdsInStates = new Set(tabStates?.map(tab => tab.uniqueId)); + const isNewTabOperationPending = + tabOperationQueue.value.some(operation => operation.type === 'newTab') || + currentlyOpenTabIds.value.some(tabId => !tabIdsInStates.has(tabId)); + + if (!isNewTabOperationPending && tabViewVisible.value) { + // To avoid unfreezing a WebView every time a tab is closed, we set the active tab index to the + // negative index of the tab that should become active if the tab view is exited via the "Done" + // button. Then in toggleTabViewWorklet(), if no new active index is provided and the active tab + // index is negative, we handling using the negative index to set the correct active tab index. + newActiveIndex = -newActiveIndex; + } else { + newActiveIndex = undefined; + } + + requestTabOperationsWorklet({ type: 'closeTab', tabId, newActiveIndex }); }, - [activeTabIndex, animatedActiveTabIndex, newTab, setTabStates, tabStates] + [animatedActiveTabIndex, currentlyOpenTabIds, newTabWorklet, requestTabOperationsWorklet, tabOperationQueue, tabStates, tabViewVisible] + ); + + useAnimatedReaction( + () => ({ + operations: tabOperationQueue.value, + shouldBlock: shouldBlockOperationQueue.value, + }), + (current, previous) => { + if (previous && current !== previous && current.operations.length > 0) { + processOperationQueueWorklet(); + } + } ); const goBack = useCallback(() => { @@ -262,11 +453,12 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode activeTabIndex, activeTabRef, animatedActiveTabIndex, - closeTab, + closeTabWorklet, + currentlyOpenTabIds, goBack, goForward, loadProgress, - newTab, + newTabWorklet, onRefresh, searchViewProgress, searchInputRef, diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index ec2be197b08..098dc1ede5e 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { FasterImageView, ImageOptions } from '@candlefinance/faster-image'; -import { Box, globalColors, useColorMode } from '@/design-system'; +import { globalColors, useColorMode } from '@/design-system'; import { useDimensions } from '@/hooks'; import React, { useCallback, useLayoutEffect, useRef } from 'react'; import { StyleSheet, View } from 'react-native'; @@ -11,8 +11,8 @@ import { TapGestureHandlerGestureEvent, } from 'react-native-gesture-handler'; import Animated, { + FadeIn, convertToRGBA, - dispatchCommand, interpolate, isColor, runOnJS, @@ -184,7 +184,8 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje activeTabIndex, activeTabRef, animatedActiveTabIndex, - closeTab, + closeTabWorklet, + currentlyOpenTabIds, loadProgress, scrollViewRef, scrollViewOffset, @@ -222,16 +223,54 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const tabUrl = tabStates?.[tabIndex]?.url; const isActiveTab = activeTabIndex === tabIndex; - const multipleTabsOpen = tabStates?.length > 1; const isOnHomepage = tabUrl === RAINBOW_HOME; - const isEmptyState = !multipleTabsOpen && isOnHomepage; const isLogoUnset = tabStates[tabIndex]?.logoUrl === undefined; + const animatedTabIndex = useSharedValue( + (currentlyOpenTabIds?.value.indexOf(tabId) === -1 + ? currentlyOpenTabIds?.value.length - 1 + : currentlyOpenTabIds?.value.indexOf(tabId)) ?? 0 + ); const screenshotData = useSharedValue(findTabScreenshot(tabId, tabUrl) || undefined); const defaultBackgroundColor = isDarkMode ? '#191A1C' : globalColors.white100; const backgroundColor = useSharedValue(defaultBackgroundColor); + const animatedTabXPosition = useDerivedValue(() => { + return withTiming( + (animatedTabIndex.value % 2) * (TAB_VIEW_COLUMN_WIDTH + 20) - (TAB_VIEW_COLUMN_WIDTH + 20) / 2, + TIMING_CONFIGS.tabPressConfig + ); + }); + + const animatedTabYPosition = useDerivedValue(() => { + return withTiming(Math.floor(animatedTabIndex.value / 2) * TAB_VIEW_ROW_HEIGHT, TIMING_CONFIGS.tabPressConfig); + }); + + const multipleTabsOpen = useDerivedValue(() => { + // The purpose of the following checks is to prevent jarring visual shifts when the tab view transitions + // from having a single tab to multiple tabs. When a second tab is created, it takes a moment for + // tabStates to catch up to currentlyOpenTabIds, and this check prevents the single tab from shifting + // due to currentlyOpenTabIds updating before the new tab component is rendered via tabStates. + const isFirstTab = currentlyOpenTabIds?.value.indexOf(tabId) === 0; + const shouldTwoTabsExist = currentlyOpenTabIds?.value.length === 2; + + const isTransitioningFromSingleToMultipleTabs = + isFirstTab && + shouldTwoTabsExist && + (tabStates?.length === 1 || (tabStates?.length === 2 && currentlyOpenTabIds?.value[1] !== tabStates?.[1]?.uniqueId)); + + const multipleTabsExist = !!(currentlyOpenTabIds?.value && currentlyOpenTabIds?.value.length > 1); + const isLastOrSecondToLastTabAndExiting = currentlyOpenTabIds?.value?.indexOf(tabId) === -1 && currentlyOpenTabIds.value.length === 1; + const multipleTabsOpen = (multipleTabsExist && !isTransitioningFromSingleToMultipleTabs) || isLastOrSecondToLastTabAndExiting; + + return multipleTabsOpen; + }); + + const animatedMultipleTabsOpen = useDerivedValue(() => { + return withTiming(multipleTabsOpen.value ? 1 : 0, TIMING_CONFIGS.tabPressConfig); + }); + const animatedWebViewBackgroundColorStyle = useAnimatedStyle(() => { const homepageColor = isDarkMode ? globalColors.grey100 : '#FBFCFD'; @@ -241,8 +280,8 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje if (isColor(backgroundColor.value)) { const rgbaColor = convertToRGBA(backgroundColor.value); - if (rgbaColor[3] < 1) { - return { backgroundColor: `rgba(${rgbaColor[0]}, ${rgbaColor[1]}, ${rgbaColor[2]}, 1)` }; + if (rgbaColor[3] < 1 && rgbaColor[3] !== 0) { + return { backgroundColor: `rgba(${rgbaColor[0] * 255}, ${rgbaColor[1] * 255}, ${rgbaColor[2] * 255}, 1)` }; } else { return { backgroundColor: backgroundColor.value }; } @@ -254,7 +293,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const animatedWebViewHeight = useDerivedValue(() => { // For some reason driving the WebView height with a separate derived // value results in slightly less tearing when the height animates - const animatedIsActiveTab = animatedActiveTabIndex?.value === tabIndex; + const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex.value; if (!animatedIsActiveTab) return COLLAPSED_WEBVIEW_HEIGHT_UNSCALED; const progress = tabViewProgress?.value || 0; @@ -269,31 +308,34 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const animatedWebViewStyle = useAnimatedStyle(() => { const progress = tabViewProgress?.value || 0; - const animatedIsActiveTab = animatedActiveTabIndex?.value === tabIndex; + const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex.value; + const isTabBeingClosed = currentlyOpenTabIds?.value?.indexOf(tabId) === -1; + const scaleDiff = 0.7 - TAB_VIEW_COLUMN_WIDTH / deviceWidth; const scale = interpolate( progress, [0, 100], - [animatedIsActiveTab ? 1 : TAB_VIEW_COLUMN_WIDTH / deviceWidth, multipleTabsOpen ? TAB_VIEW_COLUMN_WIDTH / deviceWidth : 0.7] + [animatedIsActiveTab && !isTabBeingClosed ? 1 : TAB_VIEW_COLUMN_WIDTH / deviceWidth, 0.7 - scaleDiff * animatedMultipleTabsOpen.value] ); - const xPositionStart = animatedIsActiveTab ? 0 : (tabIndex % 2) * (TAB_VIEW_COLUMN_WIDTH + 20) - (TAB_VIEW_COLUMN_WIDTH + 20) / 2; - const xPositionEnd = multipleTabsOpen ? (tabIndex % 2) * (TAB_VIEW_COLUMN_WIDTH + 20) - (TAB_VIEW_COLUMN_WIDTH + 20) / 2 : 0; + const xPositionStart = animatedIsActiveTab ? 0 : animatedTabXPosition.value; + const xPositionEnd = animatedMultipleTabsOpen.value * animatedTabXPosition.value; const xPositionForTab = interpolate(progress, [0, 100], [xPositionStart, xPositionEnd]); const extraYPadding = 20; const yPositionStart = - (animatedIsActiveTab ? 0 : Math.floor(tabIndex / 2) * TAB_VIEW_ROW_HEIGHT + extraYPadding) + + (animatedIsActiveTab ? 0 : animatedTabYPosition.value + extraYPadding) + (animatedIsActiveTab ? (1 - progress / 100) * (scrollViewOffset?.value || 0) : 0); const yPositionEnd = - (multipleTabsOpen ? Math.floor(tabIndex / 2) * TAB_VIEW_ROW_HEIGHT + extraYPadding : 0) + + (animatedTabYPosition.value + extraYPadding) * animatedMultipleTabsOpen.value + (animatedIsActiveTab ? (1 - progress / 100) * (scrollViewOffset?.value || 0) : 0); const yPositionForTab = interpolate(progress, [0, 100], [yPositionStart, yPositionEnd]); // Determine the border radius for the minimized tab that // achieves concentric corners around the close button - const invertedScale = multipleTabsOpen ? INVERTED_MULTI_TAB_SCALE : INVERTED_SINGLE_TAB_SCALE; + const invertedScaleDiff = INVERTED_SINGLE_TAB_SCALE - INVERTED_MULTI_TAB_SCALE; + const invertedScale = INVERTED_SINGLE_TAB_SCALE - invertedScaleDiff * animatedMultipleTabsOpen.value; const spaceToXButton = invertedScale * X_BUTTON_PADDING; const xButtonBorderRadius = (X_BUTTON_SIZE / 2) * invertedScale; const tabViewBorderRadius = xButtonBorderRadius + spaceToXButton; @@ -315,25 +357,26 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje // eslint-disable-next-line no-nested-ternary pointerEvents: tabViewVisible?.value ? 'box-only' : animatedIsActiveTab ? 'auto' : 'none', transform: [ - { translateY: multipleTabsOpen ? -animatedWebViewHeight.value / 2 : 0 }, + { translateY: animatedMultipleTabsOpen.value * (-animatedWebViewHeight.value / 2) }, { translateX: xPositionForTab + gestureX.value }, { translateY: yPositionForTab + gestureY.value }, { scale: scale * gestureScale.value }, - { translateY: multipleTabsOpen ? animatedWebViewHeight.value / 2 : 0 }, + { translateY: animatedMultipleTabsOpen.value * (animatedWebViewHeight.value / 2) }, ], }; }); const zIndexAnimatedStyle = useAnimatedStyle(() => { const progress = tabViewProgress?.value || 0; - const animatedIsActiveTab = animatedActiveTabIndex?.value === tabIndex; + const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex.value; + const scaleDiff = 0.7 - TAB_VIEW_COLUMN_WIDTH / deviceWidth; const scaleWeighting = gestureScale.value * interpolate( progress, [0, 100], - [animatedIsActiveTab ? 1 : TAB_VIEW_COLUMN_WIDTH / deviceWidth, multipleTabsOpen ? TAB_VIEW_COLUMN_WIDTH / deviceWidth : 0.7], + [animatedIsActiveTab ? 1 : TAB_VIEW_COLUMN_WIDTH / deviceWidth, 0.7 - scaleDiff * animatedMultipleTabsOpen.value], 'clamp' ); const zIndex = scaleWeighting * (animatedIsActiveTab || gestureScale.value > 1 ? 9999 : 1); @@ -412,8 +455,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje if (webViewRef.current !== null && isActiveTab) { activeTabRef.current = webViewRef.current; } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isActiveTab, isOnHomepage, tabId]); + }, [activeTabRef, isActiveTab, isOnHomepage, tabId]); const saveScreenshotToFileSystem = useCallback( async (tempUri: string, tabId: string, timestamp: number, url: string) => { @@ -544,7 +586,6 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje }, }); } - // eslint-disable-next-line no-empty } catch (e) { console.error('Error parsing message', e); @@ -606,15 +647,13 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje if (ctx.startX === undefined) { gestureScale.value = withTiming(1.1, TIMING_CONFIGS.tabPressConfig); - gestureY.value = withTiming(-0.05 * (multipleTabsOpen ? TAB_VIEW_TAB_HEIGHT : 0), TIMING_CONFIGS.tabPressConfig); + gestureY.value = withTiming(-0.05 * (animatedMultipleTabsOpen.value * TAB_VIEW_TAB_HEIGHT), TIMING_CONFIGS.tabPressConfig); ctx.startX = e.absoluteX; } - setNativeProps(scrollViewRef, { scrollEnabled: false }); - dispatchCommand(scrollViewRef, 'scrollTo', [0, scrollViewOffset?.value, true]); - const xDelta = e.absoluteX - ctx.startX; gestureX.value = xDelta; + setNativeProps(scrollViewRef, { scrollEnabled: false }); }, onEnd: (e, ctx: { startX?: number }) => { const xDelta = e.absoluteX - (ctx.startX || 0); @@ -622,18 +661,38 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const isBeyondDismissThreshold = xDelta < -(TAB_VIEW_COLUMN_WIDTH / 2 + 20) && e.velocityX <= 0; const isFastLeftwardSwipe = e.velocityX < -500; + const isEmptyState = !multipleTabsOpen.value && isOnHomepage; const shouldDismiss = !!tabViewVisible?.value && !isEmptyState && (isBeyondDismissThreshold || isFastLeftwardSwipe); if (shouldDismiss) { - const xDestination = -Math.min(Math.max(deviceWidth * 1.25, Math.abs(e.velocityX * 0.3)), 1000); + const xDestination = -Math.min(Math.max(deviceWidth, deviceWidth + Math.abs(e.velocityX * 0.2)), 1200); + // Store the tab's index before modifying currentlyOpenTabIds, so we can pass it along to closeTabWorklet() + const storedTabIndex = currentlyOpenTabIds?.value.indexOf(tabId) ?? tabIndex; + // Remove the tab from currentlyOpenTabIds as soon as the swipe-to-close gesture is confirmed + currentlyOpenTabIds?.modify(value => { + const index = value.indexOf(tabId); + if (index !== -1) { + value.splice(index, 1); + } + return value; + }); gestureX.value = withTiming(xDestination, TIMING_CONFIGS.tabPressConfig, () => { - runOnJS(closeTab)(tabId); + // Ensure the tab remains hidden after being swiped off screen, until the tab close operation completes gestureScale.value = 0; - gestureX.value = 0; - gestureY.value = 0; - ctx.startX = undefined; + // Once this animation completes, we know the tab is off screen and can be safely destroyed + closeTabWorklet(tabId, storedTabIndex); }); + + // In the event the last or second-to-last tab is closed, we animate its Y position to align with the + // vertical center of the single remaining tab as this tab exits and the remaining tab scales up. + const isLastOrSecondToLastTabAndExiting = + currentlyOpenTabIds?.value?.indexOf(tabId) === -1 && currentlyOpenTabIds.value.length === 1; + if (isLastOrSecondToLastTabAndExiting) { + const existingYTranslation = gestureY.value; + const scaleDiff = 0.7 - TAB_VIEW_COLUMN_WIDTH / deviceWidth; + gestureY.value = withTiming(existingYTranslation + scaleDiff * COLLAPSED_WEBVIEW_HEIGHT_UNSCALED, TIMING_CONFIGS.tabPressConfig); + } } else { gestureScale.value = withTiming(1, TIMING_CONFIGS.tabPressConfig); gestureX.value = withTiming(0, TIMING_CONFIGS.tabPressConfig); @@ -646,7 +705,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const pressTabGestureHandler = useAnimatedGestureHandler({ onActive: () => { if (tabViewVisible?.value) { - toggleTabViewWorklet(tabIndex); + toggleTabViewWorklet(animatedTabIndex.value); } }, }); @@ -656,9 +715,14 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje (current, previous) => { // Monitor changes in tabViewProgress and trigger tab screenshot capture if necessary const changesDetected = previous && current !== previous; - const isActiveTab = animatedActiveTabIndex?.value === tabIndex; - - if (isActiveTab && changesDetected && !isOnHomepage) { + const isTabBeingClosed = currentlyOpenTabIds?.value?.indexOf(tabId) === -1; + + // Note: Using the JS-side isActiveTab because this should be in sync with the WebView freeze state, + // which is driven by isActiveTab. This should allow screenshots slightly more time to capture. + if (isActiveTab && changesDetected && !isOnHomepage && !isTabBeingClosed) { + // ⚠️ TODO: Need to rewrite the enterTabViewAnimationIsComplete condition, because it assumes the + // tab animation will overshoot and rebound. If the animation config is changed, it's possible the + // screenshot condition won't be met. const enterTabViewAnimationIsComplete = tabViewVisible?.value === true && (previous || 0) > 100 && (current || 0) <= 100; const isPageLoaded = (loadProgress?.value || 0) > 0.2; @@ -679,6 +743,18 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje } ); + useAnimatedReaction( + () => ({ currentlyOpenTabIds: currentlyOpenTabIds?.value }), + current => { + const currentIndex = current.currentlyOpenTabIds?.indexOf(tabId) ?? -1; + // This allows us to give the tab its previous animated index when it's being closed, so that the close + // animation is allowed to complete with the X and Y coordinates it had based on its last real index. + if (currentIndex >= 0) { + animatedTabIndex.value = currentIndex; + } + } + ); + return ( <> {/* Need to fix some shadow performance issues - disabling shadows for now */} @@ -686,7 +762,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje {/* @ts-expect-error Property 'children' does not exist on type */} - + {/* @ts-expect-error Property 'children' does not exist on type */} ( - - )} + // 👇 This prevents the WebView from hiding its content on load/reload + renderLoading={() => <>} onLoadEnd={handleOnLoadEnd} onError={handleOnError} onShouldStartLoadWithRequest={handleShouldStartLoadWithRequest} @@ -735,15 +804,21 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje onNavigationStateChange={handleNavigationStateChange} ref={webViewRef} source={{ uri: tabUrl || RAINBOW_HOME }} - style={[styles.webViewStyle, styles.transparentBackground]} + style={styles.webViewStyle} /> )} - - - closeTab(tabId)} tabIndex={tabIndex} /> + + + @@ -772,9 +847,6 @@ const styles = StyleSheet.create({ width: deviceUtils.dimensions.width, zIndex: 20000, }, - transparentBackground: { - backgroundColor: 'transparent', - }, webViewContainer: { alignSelf: 'center', height: WEBVIEW_HEIGHT, @@ -782,9 +854,9 @@ const styles = StyleSheet.create({ position: 'absolute', top: safeAreaInsetValues.top, width: deviceUtils.dimensions.width, - zIndex: 999999999, }, webViewStyle: { + backgroundColor: 'transparent', borderCurve: 'continuous', height: WEBVIEW_HEIGHT, maxHeight: WEBVIEW_HEIGHT, diff --git a/src/components/DappBrowser/CloseTabButton.tsx b/src/components/DappBrowser/CloseTabButton.tsx index 2361b6cd5a9..33654b75b6a 100644 --- a/src/components/DappBrowser/CloseTabButton.tsx +++ b/src/components/DappBrowser/CloseTabButton.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { StyleSheet, TouchableOpacity } from 'react-native'; -import Animated, { interpolate, useAnimatedStyle, withTiming } from 'react-native-reanimated'; +import Animated, { SharedValue, interpolate, runOnUI, useAnimatedStyle, useDerivedValue, withTiming } from 'react-native-reanimated'; import { Box, Cover, TextIcon, useColorMode } from '@/design-system'; import { IS_IOS } from '@/env'; import { deviceUtils } from '@/utils'; import { AnimatedBlurView } from '@/__swaps__/screens/Swap/components/AnimatedBlurView'; -import { RAINBOW_HOME, useBrowserContext } from './BrowserContext'; -import { TAB_VIEW_COLUMN_WIDTH } from './Dimensions'; import { TIMING_CONFIGS } from '../animations/animationConfigs'; +import { useBrowserContext } from './BrowserContext'; +import { TAB_VIEW_COLUMN_WIDTH } from './Dimensions'; // ⚠️ TODO: Fix close button press detection — currently being blocked // by the gesture handlers within the BrowserTab component. @@ -24,59 +24,143 @@ const SCALE_ADJUSTED_X_BUTTON_PADDING = X_BUTTON_PADDING * INVERTED_WEBVIEW_SCAL const SCALE_ADJUSTED_X_BUTTON_SIZE_SINGLE_TAB = X_BUTTON_SIZE * SINGLE_TAB_INVERTED_WEBVIEW_SCALE; const SCALE_ADJUSTED_X_BUTTON_PADDING_SINGLE_TAB = X_BUTTON_PADDING * SINGLE_TAB_INVERTED_WEBVIEW_SCALE; -export const CloseTabButton = ({ onPress, tabIndex }: { onPress: () => void; tabIndex: number }) => { - const { animatedActiveTabIndex, tabStates, tabViewProgress, tabViewVisible } = useBrowserContext(); - const { isDarkMode } = useColorMode(); +const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity); - const multipleTabsOpen = tabStates.length > 1; - const tabUrl = tabStates[tabIndex]?.url; - const isOnHomepage = tabUrl === RAINBOW_HOME; - const isEmptyState = !multipleTabsOpen && isOnHomepage; - const buttonSize = multipleTabsOpen ? SCALE_ADJUSTED_X_BUTTON_SIZE : SCALE_ADJUSTED_X_BUTTON_SIZE_SINGLE_TAB; - const buttonPadding = multipleTabsOpen ? SCALE_ADJUSTED_X_BUTTON_PADDING : SCALE_ADJUSTED_X_BUTTON_PADDING_SINGLE_TAB; +export const CloseTabButton = ({ + animatedMultipleTabsOpen, + isOnHomepage, + multipleTabsOpen, + tabId, + tabIndex, +}: { + animatedMultipleTabsOpen: SharedValue; + isOnHomepage: boolean; + multipleTabsOpen: SharedValue; + tabId: string; + tabIndex: number; +}) => { + const { animatedActiveTabIndex, closeTabWorklet, currentlyOpenTabIds, tabViewProgress, tabViewVisible } = useBrowserContext(); + const { isDarkMode } = useColorMode(); const closeButtonStyle = useAnimatedStyle(() => { const progress = tabViewProgress?.value || 0; - const isActiveTab = animatedActiveTabIndex?.value === tabIndex; + const rawAnimatedTabIndex = currentlyOpenTabIds?.value.indexOf(tabId); + const animatedTabIndex = rawAnimatedTabIndex === -1 ? tabIndex : rawAnimatedTabIndex ?? tabIndex; + const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex; // Switch to using progress-based interpolation when the tab view is // entered. This is mainly to avoid showing the close button in the // active tab until the tab view animation is near complete. - const interpolatedOpacity = interpolate(progress, [0, 80, 100], [isActiveTab ? 0 : 1, isActiveTab ? 0 : 1, 1]); - const opacity = - !isEmptyState && (tabViewVisible?.value || !isActiveTab) ? interpolatedOpacity : withTiming(0, TIMING_CONFIGS.fastFadeConfig); + const interpolatedOpacity = interpolate(progress, [0, 80, 100], [animatedIsActiveTab ? 0 : 1, animatedIsActiveTab ? 0 : 1, 1]); + const opacity = tabViewVisible?.value || !animatedIsActiveTab ? interpolatedOpacity : withTiming(0, TIMING_CONFIGS.fastFadeConfig); + return { opacity }; }); - const pointerEventsStyle = useAnimatedStyle(() => { + const containerStyle = useAnimatedStyle(() => { + const buttonPadding = multipleTabsOpen.value ? SCALE_ADJUSTED_X_BUTTON_PADDING : SCALE_ADJUSTED_X_BUTTON_PADDING_SINGLE_TAB; + const buttonSize = multipleTabsOpen.value ? SCALE_ADJUSTED_X_BUTTON_SIZE : SCALE_ADJUSTED_X_BUTTON_SIZE_SINGLE_TAB; + + const isEmptyState = isOnHomepage && !multipleTabsOpen.value; + const opacity = isEmptyState ? withTiming(0, TIMING_CONFIGS.tabPressConfig) : withTiming(1, TIMING_CONFIGS.tabPressConfig); const pointerEvents = tabViewVisible?.value && !isEmptyState ? 'auto' : 'none'; - return { pointerEvents }; + + return { + height: buttonSize, + opacity, + pointerEvents, + right: buttonPadding, + top: buttonPadding, + width: buttonSize, + }; + }); + + const multipleTabsStyle = useAnimatedStyle(() => { + return { + opacity: interpolate(animatedMultipleTabsOpen.value, [0, 0.9, 1], [0, 0, 1], 'clamp'), + pointerEvents: multipleTabsOpen.value ? 'auto' : 'none', + }; + }); + + const singleTabStyle = useAnimatedStyle(() => { + return { + opacity: interpolate(animatedMultipleTabsOpen.value, [0, 0.1, 1], [1, 0, 0], 'clamp'), + pointerEvents: multipleTabsOpen.value ? 'none' : 'auto', + }; + }); + + const hitSlopProp = useDerivedValue(() => { + const buttonPadding = multipleTabsOpen.value ? SCALE_ADJUSTED_X_BUTTON_PADDING : SCALE_ADJUSTED_X_BUTTON_PADDING_SINGLE_TAB; + return withTiming(buttonPadding, TIMING_CONFIGS.tabPressConfig); }); return ( - - - {IS_IOS ? ( - - - - - ) : ( - - - - )} - + + runOnUI(closeTabWorklet)(tabId, tabIndex)}> + + {IS_IOS ? ( + + + + + ) : ( + + + + )} + + + {IS_IOS ? ( + + + + + ) : ( + + + + )} + + ); diff --git a/src/components/DappBrowser/TabViewToolbar.tsx b/src/components/DappBrowser/TabViewToolbar.tsx index 7839cc704be..3e5dd5d3356 100644 --- a/src/components/DappBrowser/TabViewToolbar.tsx +++ b/src/components/DappBrowser/TabViewToolbar.tsx @@ -60,9 +60,9 @@ export const TabViewToolbar = () => { }; const NewTabButton = () => { - const { newTab } = useBrowserContext(); + const { newTabWorklet } = useBrowserContext(); - return ; + return ; }; const DoneButton = () => { @@ -117,7 +117,7 @@ const BaseButton = ({ @@ -133,9 +133,11 @@ const BaseButton = ({ blurType={isDarkMode ? 'dark' : 'light'} style={[ { - zIndex: -1, - elevation: -1, + borderCurve: 'continuous', borderRadius: 22, + elevation: -1, + overflow: 'hidden', + zIndex: -1, }, position.coverAsObject, ]} @@ -146,8 +148,10 @@ const BaseButton = ({ { backgroundColor: buttonColor, borderColor: separatorSecondary, + borderCurve: 'continuous', borderRadius: 22, borderWidth: IS_IOS && isDarkMode ? THICK_BORDER_WIDTH : 0, + overflow: 'hidden', zIndex: -1, }, position.coverAsObject, diff --git a/src/components/DappBrowser/WebViewBorder.tsx b/src/components/DappBrowser/WebViewBorder.tsx index bf998c47966..5e32ce58e01 100644 --- a/src/components/DappBrowser/WebViewBorder.tsx +++ b/src/components/DappBrowser/WebViewBorder.tsx @@ -1,18 +1,20 @@ import React from 'react'; import { StyleSheet } from 'react-native'; -import Animated, { interpolate, useAnimatedStyle } from 'react-native-reanimated'; +import Animated, { SharedValue, interpolate, useAnimatedStyle } from 'react-native-reanimated'; import { Box, Cover, globalColors } from '@/design-system'; import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; import { opacity } from '@/__swaps__/screens/Swap/utils/swaps'; import { useBrowserContext } from './BrowserContext'; import { WEBVIEW_HEIGHT } from './Dimensions'; -export const WebViewBorder = ({ enabled, tabIndex }: { enabled?: boolean; tabIndex: number }) => { +export const WebViewBorder = ({ animatedTabIndex, enabled }: { animatedTabIndex: SharedValue; enabled?: boolean }) => { const { animatedActiveTabIndex, tabViewProgress } = useBrowserContext(); const webViewBorderStyle = useAnimatedStyle(() => { const progress = tabViewProgress?.value || 0; - const borderRadius = interpolate(progress, [0, 100], [animatedActiveTabIndex?.value === tabIndex ? 16 : 30, 30], 'clamp'); + const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex.value; + + const borderRadius = interpolate(progress, [0, 100], [animatedIsActiveTab ? 16 : 30, 30], 'clamp'); const opacity = 1 - progress / 100; return { diff --git a/src/components/DappBrowser/utils.ts b/src/components/DappBrowser/utils.ts index 84539501533..a55cf3ef8c3 100644 --- a/src/components/DappBrowser/utils.ts +++ b/src/components/DappBrowser/utils.ts @@ -27,6 +27,13 @@ export const generateUniqueId = (): string => { return `${timestamp}${randomString}`; }; +export function generateUniqueIdWorklet(): string { + 'worklet'; + const timestamp = Date.now().toString(36); + const randomString = Math.random().toString(36).slice(2, 7); + return `${timestamp}${randomString}`; +} + export const getNameFromFormattedUrl = (formattedUrl: string): string => { const parts = formattedUrl.split('.'); let name; From ddd0e719f4473923b3c617153405728da03a6d15 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:53:48 -0400 Subject: [PATCH 2/3] Catch up close button fixes with develop Also includes a few additional fixes --- src/components/DappBrowser/BrowserTab.tsx | 19 +- src/components/DappBrowser/CloseTabButton.tsx | 184 ++++++++++-------- src/components/DappBrowser/WebViewBorder.tsx | 21 +- 3 files changed, 129 insertions(+), 95 deletions(-) diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index 098dc1ede5e..03f2d083df5 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -204,9 +204,6 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const webViewRef = useRef(null); const viewShotRef = useRef(null); - const panRef = useRef(); - const tapRef = useRef(); - // ⚠️ TODO const gestureScale = useSharedValue(1); const gestureX = useSharedValue(0); @@ -355,7 +352,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje height: animatedWebViewHeight.value, opacity, // eslint-disable-next-line no-nested-ternary - pointerEvents: tabViewVisible?.value ? 'box-only' : animatedIsActiveTab ? 'auto' : 'none', + pointerEvents: tabViewVisible?.value ? 'auto' : animatedIsActiveTab ? 'auto' : 'none', transform: [ { translateY: animatedMultipleTabsOpen.value * (-animatedWebViewHeight.value / 2) }, { translateX: xPositionForTab + gestureX.value }, @@ -369,6 +366,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const zIndexAnimatedStyle = useAnimatedStyle(() => { const progress = tabViewProgress?.value || 0; const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex.value; + const wasCloseButtonPressed = gestureScale.value === 1 && gestureX.value < 0; const scaleDiff = 0.7 - TAB_VIEW_COLUMN_WIDTH / deviceWidth; const scaleWeighting = @@ -379,7 +377,8 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje [animatedIsActiveTab ? 1 : TAB_VIEW_COLUMN_WIDTH / deviceWidth, 0.7 - scaleDiff * animatedMultipleTabsOpen.value], 'clamp' ); - const zIndex = scaleWeighting * (animatedIsActiveTab || gestureScale.value > 1 ? 9999 : 1); + + const zIndex = scaleWeighting * (animatedIsActiveTab || gestureScale.value > 1 ? 9999 : 1) + (wasCloseButtonPressed ? 9999 : 0); return { zIndex }; }); @@ -678,9 +677,9 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje return value; }); gestureX.value = withTiming(xDestination, TIMING_CONFIGS.tabPressConfig, () => { - // Ensure the tab remains hidden after being swiped off screen, until the tab close operation completes + // Ensure the tab remains hidden after being swiped off screen (until the tab is destroyed) gestureScale.value = 0; - // Once this animation completes, we know the tab is off screen and can be safely destroyed + // Because the animation is complete we know the tab is off screen and can be safely destroyed closeTabWorklet(tabId, storedTabIndex); }); @@ -761,7 +760,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje {/* */} {/* @ts-expect-error Property 'children' does not exist on type */} - + {/* @ts-expect-error Property 'children' does not exist on type */} @@ -814,6 +811,8 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje ; + gestureX: SharedValue; + gestureY: SharedValue; isOnHomepage: boolean; multipleTabsOpen: SharedValue; tabId: string; @@ -89,80 +92,103 @@ export const CloseTabButton = ({ }; }); - const hitSlopProp = useDerivedValue(() => { - const buttonPadding = multipleTabsOpen.value ? SCALE_ADJUSTED_X_BUTTON_PADDING : SCALE_ADJUSTED_X_BUTTON_PADDING_SINGLE_TAB; - return withTiming(buttonPadding, TIMING_CONFIGS.tabPressConfig); - }); + const closeTab = useCallback(() => { + 'worklet'; + const storedTabIndex = currentlyOpenTabIds?.value.indexOf(tabId) ?? tabIndex; + + const isOnlyOneTabOpen = (currentlyOpenTabIds?.value.length || 0) === 1; + const isTabInLeftColumn = storedTabIndex % 2 === 0 && !isOnlyOneTabOpen; + const xDestination = isTabInLeftColumn ? -deviceUtils.dimensions.width / 1.5 : -deviceUtils.dimensions.width; + + currentlyOpenTabIds?.modify(value => { + const index = value.indexOf(tabId); + if (index !== -1) { + value.splice(index, 1); + } + return value; + }); + gestureX.value = withTiming(xDestination, TIMING_CONFIGS.tabPressConfig, () => { + // Because the animation is complete we know the tab is off screen and can be safely destroyed + closeTabWorklet(tabId, storedTabIndex); + }); + + // In the event the last or second-to-last tab is closed, we animate its Y position to align with the + // vertical center of the single remaining tab as this tab exits and the remaining tab scales up. + const isLastOrSecondToLastTabAndExiting = currentlyOpenTabIds?.value?.indexOf(tabId) === -1 && currentlyOpenTabIds.value.length === 1; + if (isLastOrSecondToLastTabAndExiting) { + const existingYTranslation = gestureY.value; + const scaleDiff = 0.7 - TAB_VIEW_COLUMN_WIDTH / deviceUtils.dimensions.width; + gestureY.value = withTiming(existingYTranslation + scaleDiff * COLLAPSED_WEBVIEW_HEIGHT_UNSCALED, TIMING_CONFIGS.tabPressConfig); + } + }, [closeTabWorklet, currentlyOpenTabIds, gestureX, gestureY, tabId, tabIndex]); return ( - - - runOnUI(closeTabWorklet)(tabId, tabIndex)}> - - {IS_IOS ? ( - - - - - ) : ( + + + + {IS_IOS ? ( + - - - )} - - - {IS_IOS ? ( + background="fillSecondary" + borderRadius={SCALE_ADJUSTED_X_BUTTON_SIZE_SINGLE_TAB / 2} + height="full" + position="absolute" + width="full" + /> + + + ) : ( + + + + )} + + + {IS_IOS ? ( + - - - - ) : ( - - - - )} - - - - + background="fillSecondary" + borderRadius={SCALE_ADJUSTED_X_BUTTON_SIZE / 2} + height="full" + position="absolute" + width="full" + /> + + + ) : ( + + + + )} + + + ); }; @@ -185,10 +211,8 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - closeButtonWrapperStyle: { - position: 'absolute', - }, containerStyle: { + position: 'absolute', zIndex: 99999999999, }, }); diff --git a/src/components/DappBrowser/WebViewBorder.tsx b/src/components/DappBrowser/WebViewBorder.tsx index 5e32ce58e01..e93dec34972 100644 --- a/src/components/DappBrowser/WebViewBorder.tsx +++ b/src/components/DappBrowser/WebViewBorder.tsx @@ -8,9 +8,15 @@ import { useBrowserContext } from './BrowserContext'; import { WEBVIEW_HEIGHT } from './Dimensions'; export const WebViewBorder = ({ animatedTabIndex, enabled }: { animatedTabIndex: SharedValue; enabled?: boolean }) => { - const { animatedActiveTabIndex, tabViewProgress } = useBrowserContext(); + const { animatedActiveTabIndex, tabViewProgress, tabViewVisible } = useBrowserContext(); const webViewBorderStyle = useAnimatedStyle(() => { + if (!enabled) { + return { + pointerEvents: tabViewVisible?.value ? 'auto' : 'none', + }; + } + const progress = tabViewProgress?.value || 0; const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex.value; @@ -20,14 +26,20 @@ export const WebViewBorder = ({ animatedTabIndex, enabled }: { animatedTabIndex: return { borderRadius, opacity, + pointerEvents: tabViewVisible?.value ? 'auto' : 'none', }; }); - return enabled ? ( + return ( - + - ) : null; + ); }; const styles = StyleSheet.create({ @@ -38,7 +50,6 @@ const styles = StyleSheet.create({ borderRadius: 16, borderWidth: THICK_BORDER_WIDTH, overflow: 'hidden', - pointerEvents: 'none', }, zIndexStyle: { zIndex: 30000, From 799d89e02466a173a0eea80cf428bbb7260fca3d Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:59:16 -0400 Subject: [PATCH 3/3] Merge remote-tracking branch 'origin/@bruno/fix-close-tab-btn' into @christian/fix-close-tab-btn --- .../components/GestureHandlerV1Button.tsx | 29 ++++++++++--------- src/components/DappBrowser/BrowserTab.tsx | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx index 09d0a652164..eaae75d6cd4 100644 --- a/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx +++ b/src/__swaps__/screens/Swap/components/GestureHandlerV1Button.tsx @@ -55,17 +55,20 @@ type GestureHandlerButtonProps = { * }; * ``` */ -export function GestureHandlerV1Button({ - children, - disableButtonPressWrapper = false, - disabled = false, - onPressJS, - onPressStartWorklet, - onPressWorklet, - pointerEvents = 'box-only', - scaleTo = 0.86, - style, -}: GestureHandlerButtonProps) { +export const GestureHandlerV1Button = React.forwardRef(function GestureHandlerV1Button( + { + children, + disableButtonPressWrapper = false, + disabled = false, + onPressJS, + onPressStartWorklet, + onPressWorklet, + pointerEvents = 'box-only', + scaleTo = 0.86, + style, + }: GestureHandlerButtonProps, + forwardedRef: React.LegacyRef | undefined +) { const pressHandler = useAnimatedGestureHandler({ onStart: () => { if (onPressStartWorklet) onPressStartWorklet(); @@ -86,11 +89,11 @@ export function GestureHandlerV1Button({ )} > {/* @ts-expect-error Property 'children' does not exist on type */} - + {children} ); -} +}); diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index 03f2d083df5..5aedf139f03 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -347,11 +347,11 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const opacity = interpolate(progress, [0, 100], [animatedIsActiveTab ? 1 : 0, 1], 'clamp'); + // eslint-disable-next-line no-nested-ternary return { borderRadius, height: animatedWebViewHeight.value, opacity, - // eslint-disable-next-line no-nested-ternary pointerEvents: tabViewVisible?.value ? 'auto' : animatedIsActiveTab ? 'auto' : 'none', transform: [ { translateY: animatedMultipleTabsOpen.value * (-animatedWebViewHeight.value / 2) },