From 02d5e900bf3a2e4bd5b55baa74d59a29faddccb0 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:12:30 -0400 Subject: [PATCH 01/18] Add an extra guard to prevent currentlyOpenTabIds/tabStates from becoming out of sync --- src/components/DappBrowser/BrowserContext.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/DappBrowser/BrowserContext.tsx b/src/components/DappBrowser/BrowserContext.tsx index e06d85a493e..57a4b4484c3 100644 --- a/src/components/DappBrowser/BrowserContext.tsx +++ b/src/components/DappBrowser/BrowserContext.tsx @@ -325,6 +325,14 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode animatedActiveTabIndex.value = indexToSet; } + // Remove any remaining tabs that exist in tabStates but not in currentlyOpenTabIds. This covers + // cases where tabStates hasn't yet been updated between tab close operations. + for (let i = newTabStates.length - 1; i >= 0; i--) { + if (!currentlyOpenTabIds.value.includes(newTabStates[i].uniqueId)) { + newTabStates.splice(i, 1); + } + } + runOnJS(setTabStatesThenUnblockQueue)( newTabStates, shouldToggleTabView, From f39fb428ab6570263da0c1cd6e009ec4e60f9cb1 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:16:53 -0400 Subject: [PATCH 02/18] Minor context tweaks, add closeAllTabsWorklet --- src/components/DappBrowser/BrowserContext.tsx | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/src/components/DappBrowser/BrowserContext.tsx b/src/components/DappBrowser/BrowserContext.tsx index 57a4b4484c3..f8b8b757405 100644 --- a/src/components/DappBrowser/BrowserContext.tsx +++ b/src/components/DappBrowser/BrowserContext.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ import React, { createContext, useCallback, useContext, useRef, useState } from 'react'; import { TextInput } from 'react-native'; import isEqual from 'react-fast-compare'; @@ -40,13 +39,14 @@ interface BrowserContextType { activeTabIndex: number; activeTabRef: React.MutableRefObject; animatedActiveTabIndex: SharedValue | undefined; + closeAllTabsWorklet: () => void; closeTabWorklet: (tabId: string, tabIndex: number) => void; currentlyOpenTabIds: SharedValue | undefined; getActiveTabState: () => TabState | undefined; goBack: () => void; goForward: () => void; loadProgress: SharedValue | undefined; - newTabWorklet: (url?: string) => void; + newTabWorklet: (newTabUrl?: string) => void; onRefresh: () => void; searchInputRef: React.RefObject; searchViewProgress: SharedValue | undefined; @@ -70,7 +70,7 @@ export interface TabState { type TabOperationType = 'newTab' | 'closeTab'; -interface TabOperation { +interface BaseTabOperation { type: TabOperationType; tabId: string; newActiveIndex: number | undefined; @@ -78,6 +78,16 @@ interface TabOperation { } export const RAINBOW_HOME = 'RAINBOW_HOME'; +interface CloseTabOperation extends BaseTabOperation { + type: 'closeTab'; +} + +interface NewTabOperation extends BaseTabOperation { + type: 'newTab'; + newTabUrl?: string; +} + +type TabOperation = CloseTabOperation | NewTabOperation; const DEFAULT_TAB_STATE: TabState[] = [{ canGoBack: false, canGoForward: false, uniqueId: generateUniqueId(), url: RAINBOW_HOME }]; @@ -85,6 +95,9 @@ const DEFAULT_BROWSER_CONTEXT: BrowserContextType = { activeTabIndex: 0, activeTabRef: { current: null }, animatedActiveTabIndex: undefined, + closeAllTabsWorklet: () => { + return; + }, closeTabWorklet: () => { return; }, @@ -98,7 +111,7 @@ const DEFAULT_BROWSER_CONTEXT: BrowserContextType = { goForward: () => { return; }, - newTabWorklet: (url?: string) => { + newTabWorklet: () => { return; }, onRefresh: () => { @@ -243,6 +256,7 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode } else if (indexToMakeActive !== undefined) { setActiveTabIndex(indexToMakeActive); } + shouldBlockOperationQueue.value = false; }, [setTabStates, shouldBlockOperationQueue, toggleTabViewWorklet] @@ -282,9 +296,6 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode 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); @@ -301,7 +312,7 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode canGoBack: false, canGoForward: false, uniqueId: operation.tabId, - url: operation.url || RAINBOW_HOME, + url: operation.newTabUrl || RAINBOW_HOME, }; newTabStates.push(newTab); shouldToggleTabView = true; @@ -315,6 +326,7 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode } } + // Double check to ensure the newActiveIndex is valid if (newActiveIndex !== undefined && (tabViewVisible.value || newActiveIndex >= 0)) { animatedActiveTabIndex.value = newActiveIndex; } else { @@ -356,7 +368,7 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode ]); const newTabWorklet = useCallback( - (url?: string) => { + (newTabUrl?: string) => { 'worklet'; const tabIdsInStates = new Set(tabStates?.map(state => state.uniqueId)); const isNewTabOperationPending = @@ -366,7 +378,7 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode // 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 (url || (!isNewTabOperationPending && (tabViewVisible.value || currentlyOpenTabIds.value.length === 0))) { + if (newTabUrl || (!isNewTabOperationPending && (tabViewVisible.value || currentlyOpenTabIds.value.length === 0))) { const tabIdForNewTab = generateUniqueIdWorklet(); const newActiveIndex = currentlyOpenTabIds.value.length - 1; @@ -374,12 +386,23 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode value.push(tabIdForNewTab); return value; }); - requestTabOperationsWorklet({ type: 'newTab', tabId: tabIdForNewTab, newActiveIndex, url }); + requestTabOperationsWorklet({ type: 'newTab', tabId: tabIdForNewTab, newActiveIndex, newTabUrl }); } }, [currentlyOpenTabIds, requestTabOperationsWorklet, tabOperationQueue, tabStates, tabViewVisible] ); + const closeAllTabsWorklet = useCallback(() => { + 'worklet'; + const tabsToClose: TabOperation[] = currentlyOpenTabIds.value.map(tabId => ({ type: 'closeTab', tabId, newActiveIndex: undefined })); + currentlyOpenTabIds.modify(value => { + value.splice(0, value.length); + return value; + }); + requestTabOperationsWorklet(tabsToClose); + newTabWorklet(); + }, [currentlyOpenTabIds, newTabWorklet, requestTabOperationsWorklet]); + const closeTabWorklet = useCallback( (tabId: string, tabIndex: number) => { 'worklet'; @@ -463,13 +486,14 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode activeTabIndex, activeTabRef, animatedActiveTabIndex, + closeAllTabsWorklet, closeTabWorklet, + currentlyOpenTabIds, getActiveTabState, goBack, goForward, loadProgress, newTabWorklet, - currentlyOpenTabIds, onRefresh, searchViewProgress, searchInputRef, From 5cdd3401afe6a46022f7303d273b23fea7fb5ae0 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:25:17 -0400 Subject: [PATCH 03/18] Move RAINBOW_HOME to constants --- src/components/DappBrowser/BrowserContext.tsx | 2 +- src/components/DappBrowser/BrowserTab.tsx | 4 ++-- src/components/DappBrowser/constants.ts | 1 + src/components/DappBrowser/search/SearchBar.tsx | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/DappBrowser/BrowserContext.tsx b/src/components/DappBrowser/BrowserContext.tsx index f8b8b757405..d7d9bc77f93 100644 --- a/src/components/DappBrowser/BrowserContext.tsx +++ b/src/components/DappBrowser/BrowserContext.tsx @@ -15,6 +15,7 @@ import Animated, { } from 'react-native-reanimated'; import WebView from 'react-native-webview'; import { SPRING_CONFIGS } from '@/components/animations/animationConfigs'; +import { RAINBOW_HOME } from './constants'; import { generateUniqueId, generateUniqueIdWorklet } from './utils'; interface BrowserTabViewProgressContextType { @@ -77,7 +78,6 @@ interface BaseTabOperation { url?: string; } -export const RAINBOW_HOME = 'RAINBOW_HOME'; interface CloseTabOperation extends BaseTabOperation { type: 'closeTab'; } diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index d6033a42572..49f1b3c97d7 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -29,7 +29,7 @@ import ViewShot from 'react-native-view-shot'; import WebView, { WebViewMessageEvent, WebViewNavigation } from 'react-native-webview'; import { deviceUtils } from '@/utils'; import { MMKV } from 'react-native-mmkv'; -import { RAINBOW_HOME, TabState, useBrowserContext } from './BrowserContext'; +import { TabState, useBrowserContext } from './BrowserContext'; import { Freeze } from 'react-freeze'; import { COLLAPSED_WEBVIEW_HEIGHT_UNSCALED, @@ -51,9 +51,9 @@ import Homepage from './Homepage'; import { handleProviderRequestApp } from './handleProviderRequest'; import { WebViewBorder } from './WebViewBorder'; import { SPRING_CONFIGS, TIMING_CONFIGS } from '../animations/animationConfigs'; -import { FASTER_IMAGE_CONFIG } from './constants'; import { RainbowError, logger } from '@/logger'; import { isEmpty } from 'lodash'; +import { FASTER_IMAGE_CONFIG, RAINBOW_HOME } from './constants'; // ⚠️ TODO: Split this file apart into hooks, smaller components // useTabScreenshots, useAnimatedWebViewStyles, useWebViewGestures diff --git a/src/components/DappBrowser/constants.ts b/src/components/DappBrowser/constants.ts index c4772e7c5a5..3c807c747fc 100644 --- a/src/components/DappBrowser/constants.ts +++ b/src/components/DappBrowser/constants.ts @@ -3,6 +3,7 @@ import { ImageOptions } from '@candlefinance/faster-image'; export const GOOGLE_SEARCH_URL = 'https://www.google.com/search?q='; export const HTTP = 'http://'; export const HTTPS = 'https://'; +export const RAINBOW_HOME = 'RAINBOW_HOME'; const BLANK_BASE64_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; diff --git a/src/components/DappBrowser/search/SearchBar.tsx b/src/components/DappBrowser/search/SearchBar.tsx index cd240e730a9..d16ba09c5aa 100644 --- a/src/components/DappBrowser/search/SearchBar.tsx +++ b/src/components/DappBrowser/search/SearchBar.tsx @@ -15,8 +15,8 @@ import { IS_IOS } from '@/env'; import { useKeyboardHeight, useDimensions } from '@/hooks'; import * as i18n from '@/languages'; import { TAB_BAR_HEIGHT } from '@/navigation/SwipeNavigator'; -import { RAINBOW_HOME, useBrowserContext } from '../BrowserContext'; -import { GOOGLE_SEARCH_URL, HTTP, HTTPS } from '../constants'; +import { useBrowserContext } from '../BrowserContext'; +import { GOOGLE_SEARCH_URL, HTTP, HTTPS, RAINBOW_HOME } from '../constants'; import { AccountIcon } from '../search-input/AccountIcon'; import { SearchInput } from '../search-input/SearchInput'; import { TabButton } from '../search-input/TabButton'; From 61d9d365dc12dcdb182736724e30038359a3445c Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:27:29 -0400 Subject: [PATCH 04/18] Adjust tab transitions animation timing --- src/components/animations/animationConfigs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/animations/animationConfigs.ts b/src/components/animations/animationConfigs.ts index b73e783f0bb..1134b3e360c 100644 --- a/src/components/animations/animationConfigs.ts +++ b/src/components/animations/animationConfigs.ts @@ -27,7 +27,7 @@ const timingAnimations = createTimingConfigs({ fastFadeConfig: { duration: 100, easing: Easing.bezier(0.22, 1, 0.36, 1) }, slowFadeConfig: { duration: 300, easing: Easing.bezier(0.22, 1, 0.36, 1) }, slowestFadeConfig: { duration: 500, easing: Easing.bezier(0.22, 1, 0.36, 1) }, - tabPressConfig: { duration: 750, easing: Easing.bezier(0.22, 1, 0.36, 1) }, + tabPressConfig: { duration: 800, easing: Easing.bezier(0.22, 1, 0.36, 1) }, }); export const TIMING_CONFIGS: Record = timingAnimations; From 3aea1b3e8e19efef00631dbe0b23db4155ca1dbf Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:39:05 -0400 Subject: [PATCH 05/18] Improve website logo detection, consolidate metadata messages --- src/components/DappBrowser/BrowserTab.tsx | 43 ++++++++------------ src/components/DappBrowser/scripts.ts | 48 +++++++++++++++++++++++ 2 files changed, 65 insertions(+), 26 deletions(-) create mode 100644 src/components/DappBrowser/scripts.ts diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index 49f1b3c97d7..30cee6bae9b 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -52,8 +52,8 @@ import { handleProviderRequestApp } from './handleProviderRequest'; import { WebViewBorder } from './WebViewBorder'; import { SPRING_CONFIGS, TIMING_CONFIGS } from '../animations/animationConfigs'; import { RainbowError, logger } from '@/logger'; -import { isEmpty } from 'lodash'; import { FASTER_IMAGE_CONFIG, RAINBOW_HOME } from './constants'; +import { getWebsiteMetadata } from './scripts'; // ⚠️ TODO: Split this file apart into hooks, smaller components // useTabScreenshots, useAnimatedWebViewStyles, useWebViewGestures @@ -168,18 +168,6 @@ const deletePrunedScreenshotFiles = async (allScreenshots: ScreenshotType[], scr } }; -const getWebsiteBackgroundColorAndTitle = ` - const bgColor = window.getComputedStyle(document.body, null).getPropertyValue('background-color'); - let appleTouchIconHref = document.querySelector("link[rel='apple-touch-icon']")?.getAttribute('href'); - if (appleTouchIconHref && !appleTouchIconHref.startsWith('http')) { - appleTouchIconHref = window.location.origin + appleTouchIconHref; - } - window.ReactNativeWebView.postMessage(JSON.stringify({ topic: "bg", payload: bgColor})); - window.ReactNativeWebView.postMessage(JSON.stringify({ topic: "title", payload: document.title })); - window.ReactNativeWebView.postMessage(JSON.stringify({ topic: "logo", payload: appleTouchIconHref })); - true; - `; - export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, injectedJS }: BrowserTabProps) { const { activeTabIndex, @@ -222,7 +210,6 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const tabUrl = tabStates?.[tabIndex]?.url; const isActiveTab = activeTabIndex === tabIndex; const isOnHomepage = tabUrl === RAINBOW_HOME; - const isLogoUnset = tabStates[tabIndex]?.logoUrl === undefined; const animatedTabIndex = useSharedValue( (currentlyOpenTabIds?.value.indexOf(tabId) === -1 @@ -384,9 +371,8 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const handleNavigationStateChange = useCallback( (navState: WebViewNavigation) => { - // Set the logo if it's not already set for the current website - // ⚠️ TODO: Modify this to check against the root domain or subdomain+domain - if ((isLogoUnset && !isEmpty(logo.current)) || navState.url !== tabStates[tabIndex].url) { + // Set the logo if it's not already set to the current website's logo + if (tabStates[tabIndex].logoUrl !== logo.current) { updateActiveTabState( { logoUrl: logo.current, @@ -445,7 +431,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje ); } }, - [isLogoUnset, tabId, logo, tabIndex, tabStates, updateActiveTabState] + [logo, tabId, tabIndex, tabStates, updateActiveTabState] ); // useLayoutEffect seems to more reliably assign the ref correctly @@ -561,14 +547,19 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje // validate message and parse data const parsedData = typeof data === 'string' ? JSON.parse(data) : data; if (!parsedData || (!parsedData.topic && !parsedData.payload)) return; - if (parsedData.topic === 'bg') { - if (typeof parsedData.payload === 'string') { - backgroundColor.value = parsedData.payload; + + if (parsedData.topic === 'websiteMetadata') { + const { bgColor, logoUrl, pageTitle } = parsedData.payload; + + if (bgColor && typeof bgColor === 'string') { + backgroundColor.value = bgColor; + } + if (logoUrl && typeof logoUrl === 'string') { + logo.current = logoUrl; + } + if (pageTitle && typeof pageTitle === 'string') { + title.current = pageTitle; } - } else if (parsedData.topic === 'title') { - title.current = parsedData.payload; - } else if (parsedData.topic === 'logo') { - logo.current = parsedData.payload; } else { const m = currentMessenger.current; handleProviderRequestApp({ @@ -787,7 +778,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje automaticallyAdjustContentInsets automaticallyAdjustsScrollIndicatorInsets={false} decelerationRate={'normal'} - injectedJavaScript={getWebsiteBackgroundColorAndTitle} + injectedJavaScript={getWebsiteMetadata} mediaPlaybackRequiresUserAction onLoadStart={handleOnLoadStart} onLoad={handleOnLoad} diff --git a/src/components/DappBrowser/scripts.ts b/src/components/DappBrowser/scripts.ts new file mode 100644 index 00000000000..86fdacfaa2f --- /dev/null +++ b/src/components/DappBrowser/scripts.ts @@ -0,0 +1,48 @@ +export const getWebsiteMetadata = ` + const bgColor = window.getComputedStyle(document.body, null).getPropertyValue('background-color') || undefined; + + const icons = Array.from(document.querySelectorAll("link[rel='apple-touch-icon'], link[rel='shortcut icon'], link[rel='icon'], link[rel='icon'][type='image/svg+xml']")); + let highestResIcon = { href: undefined, size: 0 }; + + for (const icon of icons) { + const iconHref = icon.getAttribute('href'); + if (icon.type === 'image/svg+xml') { + highestResIcon = { href: iconHref, size: 1000 }; + break; + } else { + const sizeAttribute = icon.getAttribute('sizes'); + if (sizeAttribute) { + const size = Math.max(...sizeAttribute.split('x').map(num => parseInt(num, 10))); + if (size > highestResIcon.size) { + highestResIcon = { href: iconHref, size: size }; + if (size >= 180) break; + } + } else if (icon.rel === 'apple-touch-icon') { + highestResIcon = { href: iconHref, size: 180 }; + } else if (iconHref && !highestResIcon.href) { + highestResIcon = { href: iconHref, size: 0 }; + } + } + } + + let logoUrl; + if (highestResIcon.href) { + logoUrl = highestResIcon.href.startsWith('http') ? highestResIcon.href : window.location.origin + highestResIcon.href; + } else { + logoUrl = undefined; + } + + const pageTitle = document.title || undefined; + + const websiteMetadata = { + topic: "websiteMetadata", + payload: { + bgColor: bgColor, + logoUrl: logoUrl, + pageTitle: pageTitle, + } + }; + + window.ReactNativeWebView.postMessage(JSON.stringify(websiteMetadata)); + true; + `; From 66d6197bf6356d14e13f6bc6f71acb7020fd8123 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:45:26 -0400 Subject: [PATCH 06/18] Invisibly autoscroll the tab view to center the active tab As soon as a tab becomes fully open, the tab view is invisibly scrolled to ensure that if the tab view is re-entered, the active tab is centered vertically on the screen (or as close as possible if the tab is near the top or bottom of the list) --- src/components/DappBrowser/BrowserTab.tsx | 62 +++++++++++++++++++---- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index 30cee6bae9b..3ec460b25e6 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -13,6 +13,7 @@ import { import Animated, { FadeIn, convertToRGBA, + dispatchCommand, interpolate, isColor, runOnJS, @@ -27,7 +28,7 @@ import Animated, { } from 'react-native-reanimated'; import ViewShot from 'react-native-view-shot'; import WebView, { WebViewMessageEvent, WebViewNavigation } from 'react-native-webview'; -import { deviceUtils } from '@/utils'; +import { deviceUtils, safeAreaInsetValues } from '@/utils'; import { MMKV } from 'react-native-mmkv'; import { TabState, useBrowserContext } from './BrowserContext'; import { Freeze } from 'react-freeze'; @@ -709,25 +710,64 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje // 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) { + if (isActiveTab && changesDetected && !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; - if (!enterTabViewAnimationIsComplete || !isPageLoaded) return; + if (enterTabViewAnimationIsComplete && isPageLoaded && !isOnHomepage) { + const previousScreenshotExists = !!screenshotData.value?.uri; + const tabIdChanged = screenshotData.value?.id !== tabId; + const urlChanged = screenshotData.value?.url !== tabUrl; + const oneMinuteAgo = Date.now() - 1000 * 60; + const isScreenshotStale = screenshotData.value && screenshotData.value?.timestamp < oneMinuteAgo; - const previousScreenshotExists = !!screenshotData.value?.uri; - const tabIdChanged = screenshotData.value?.id !== tabId; - const urlChanged = screenshotData.value?.url !== tabUrl; - const oneMinuteAgo = Date.now() - 1000 * 60; - const isScreenshotStale = screenshotData.value && screenshotData.value?.timestamp < oneMinuteAgo; + const shouldCaptureScreenshot = !previousScreenshotExists || tabIdChanged || urlChanged || isScreenshotStale; - const shouldCaptureScreenshot = !previousScreenshotExists || tabIdChanged || urlChanged || isScreenshotStale; + if (shouldCaptureScreenshot) { + runOnJS(captureAndSaveScreenshot)(); + } + } + + // If necessary, invisibly scroll to the currently active tab when the tab view is fully closed + const isScrollViewScrollable = (currentlyOpenTabIds?.value.length || 0) > 4; + const exitTabViewAnimationIsComplete = + isScrollViewScrollable && tabViewVisible?.value === false && current === 0 && previous && previous !== 0; + + if (exitTabViewAnimationIsComplete && isScrollViewScrollable) { + consoleLogWorklet('SCROLLING TAB INTO POSITION'); + + const currentTabRow = Math.floor(animatedTabIndex.value / 2); + const scrollViewHeight = + Math.ceil((currentlyOpenTabIds?.value.length || 0) / 2) * TAB_VIEW_ROW_HEIGHT + + safeAreaInsetValues.bottom + + 165 + + 28 + + (IS_IOS ? 0 : 35); + + const screenHeight = deviceUtils.dimensions.height; + const halfScreenHeight = screenHeight / 2; + const tabCenterPosition = currentTabRow * TAB_VIEW_ROW_HEIGHT + (currentTabRow - 1) * 28 + TAB_VIEW_ROW_HEIGHT / 2 + 37; + + let scrollPositionToCenterCurrentTab; + + if (scrollViewHeight <= screenHeight) { + // No need to scroll if all tabs fit on the screen + scrollPositionToCenterCurrentTab = 0; + } else if (tabCenterPosition <= halfScreenHeight) { + // Scroll to top if the tab is too near to the top of the scroll view to be centered + scrollPositionToCenterCurrentTab = 0; + } else if (tabCenterPosition + halfScreenHeight >= scrollViewHeight) { + // Scroll to bottom if the tab is too near to the end of the scroll view to be centered + scrollPositionToCenterCurrentTab = scrollViewHeight - screenHeight; + } else { + // Otherwise, vertically center the tab on the screen + scrollPositionToCenterCurrentTab = tabCenterPosition - halfScreenHeight; + } - if (shouldCaptureScreenshot) { - runOnJS(captureAndSaveScreenshot)(); + dispatchCommand(scrollViewRef, 'scrollTo', [0, scrollPositionToCenterCurrentTab, false]); } } } From e267e471c7e1e6ffe522a1ab444b4f90e26e6fcb Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:04:49 -0400 Subject: [PATCH 07/18] =?UTF-8?q?formattedInputValue.value=20=E2=86=92=20f?= =?UTF-8?q?ormattedInputValue.url?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DappBrowser/search-input/SearchInput.tsx | 6 +++--- src/components/DappBrowser/search/SearchBar.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/DappBrowser/search-input/SearchInput.tsx b/src/components/DappBrowser/search-input/SearchInput.tsx index 10d6bb4518c..aabad9c7c62 100644 --- a/src/components/DappBrowser/search-input/SearchInput.tsx +++ b/src/components/DappBrowser/search-input/SearchInput.tsx @@ -41,7 +41,7 @@ export const SearchInput = ({ canGoForward, }: { inputRef: RefObject; - formattedInputValue: { value: string; tabIndex: number }; + formattedInputValue: { url: string; tabIndex: number }; inputValue: string | undefined; isGoogleSearch: boolean; isHome: boolean; @@ -67,9 +67,9 @@ export const SearchInput = ({ const buttonColorAndroid = isDarkMode ? globalColors.blueGrey100 : globalColors.white100; const buttonColor = IS_IOS ? buttonColorIOS : buttonColorAndroid; - const formattedUrl = formattedInputValue?.value; + const formattedUrl = formattedInputValue?.url; const formattedUrlValue = useDerivedValue(() => { - return formattedInputValue?.tabIndex !== animatedActiveTabIndex?.value ? '' : formattedInputValue?.value; + return formattedInputValue?.tabIndex !== animatedActiveTabIndex?.value ? '' : formattedInputValue?.url; }); const pointerEventsStyle = useAnimatedStyle(() => ({ diff --git a/src/components/DappBrowser/search/SearchBar.tsx b/src/components/DappBrowser/search/SearchBar.tsx index d16ba09c5aa..ebc78f00fec 100644 --- a/src/components/DappBrowser/search/SearchBar.tsx +++ b/src/components/DappBrowser/search/SearchBar.tsx @@ -43,7 +43,7 @@ export const SearchBar = () => { const formattedInputValue = useMemo(() => { if (isHome) { - return { value: i18n.t(i18n.l.dapp_browser.address_bar.input_placeholder), tabIndex: activeTabIndex }; + return { url: i18n.t(i18n.l.dapp_browser.address_bar.input_placeholder), tabIndex: activeTabIndex }; } let formattedValue = ''; @@ -60,12 +60,12 @@ export const SearchBar = () => { formattedValue = url; } } - return { value: formattedValue, tabIndex: activeTabIndex }; + return { url: formattedValue, tabIndex: activeTabIndex }; }, [activeTabIndex, isGoogleSearch, isHome, url]); const urlWithoutTrailingSlash = url?.endsWith('/') ? url.slice(0, -1) : url; // eslint-disable-next-line no-nested-ternary - const inputValue = isHome ? undefined : isGoogleSearch ? formattedInputValue.value : urlWithoutTrailingSlash; + const inputValue = isHome ? undefined : isGoogleSearch ? formattedInputValue.url : urlWithoutTrailingSlash; const barStyle = useAnimatedStyle(() => { const progress = tabViewProgress?.value ?? 0; From f39506d360d7145ef776a1b3907944789278a508 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:05:05 -0400 Subject: [PATCH 08/18] Improve favorites URL standardization --- src/state/favoriteDapps/index.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/state/favoriteDapps/index.ts b/src/state/favoriteDapps/index.ts index e31c1ca169b..8298eba7fb4 100644 --- a/src/state/favoriteDapps/index.ts +++ b/src/state/favoriteDapps/index.ts @@ -1,5 +1,4 @@ import create from 'zustand'; -import { HTTPS } from '@/components/DappBrowser/constants'; import { createStore } from '../internal/createStore'; // need to combine types here @@ -16,7 +15,16 @@ interface FavoriteDappsStore { isFavorite: (url: string) => boolean; } -const standardizeUrl = (url: string) => (/^https?:\/\//.test(url) ? url : `${HTTPS}${url}`); +const standardizeUrl = (url: string) => { + // Strips the URL down from e.g. "https://www.rainbow.me/app/" to "rainbow.me/app" + let standardizedUrl = url?.trim(); + standardizedUrl = standardizedUrl?.replace(/^https?:\/\//, ''); + standardizedUrl = standardizedUrl?.replace(/^www\./, ''); + if (standardizedUrl?.endsWith('/')) { + standardizedUrl = standardizedUrl?.slice(0, -1); + } + return standardizedUrl; +}; export const favoriteDappsStore = createStore( (set, get) => ({ @@ -43,7 +51,7 @@ export const favoriteDappsStore = createStore( { persist: { name: 'favoriteDapps', - version: 1, + version: 2, }, } ); From dbba15f8eaf3ae2c0fc81524ed8c3927038c83b2 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:06:38 -0400 Subject: [PATCH 09/18] Fix AccountIcon positioning --- src/components/DappBrowser/search/SearchBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DappBrowser/search/SearchBar.tsx b/src/components/DappBrowser/search/SearchBar.tsx index ebc78f00fec..433fb7892ec 100644 --- a/src/components/DappBrowser/search/SearchBar.tsx +++ b/src/components/DappBrowser/search/SearchBar.tsx @@ -156,7 +156,7 @@ export const SearchBar = () => { style={[{ alignItems: 'center', flexDirection: 'row', justifyContent: 'center' }, barStyle]} width="full" > - + From caec2a2c090e9a5c079f65b3720a6706ba75d095 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:24:17 -0400 Subject: [PATCH 10/18] Include logo in existing tab state updates --- src/components/DappBrowser/BrowserTab.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index 3ec460b25e6..ceb07d10e22 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -406,6 +406,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje { canGoBack: navState.canGoBack, canGoForward: navState.canGoForward, + logoUrl: logo.current, url: navState.url, }, tabId @@ -416,6 +417,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje { canGoBack: navState.canGoBack, canGoForward: navState.canGoForward, + logoUrl: logo.current, }, tabId ); @@ -427,6 +429,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje { canGoBack: navState.canGoBack, canGoForward: navState.canGoForward, + logoUrl: logo.current, }, tabId ); From 8bc77eab5bd33a3150fcc1a3566e994fa99d41f8 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:31:51 -0400 Subject: [PATCH 11/18] Animate tab view ScrollView height changes --- src/components/DappBrowser/DappBrowser.tsx | 33 +++++++++++++++------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/components/DappBrowser/DappBrowser.tsx b/src/components/DappBrowser/DappBrowser.tsx index ee706166e73..cbd2cf77ecc 100644 --- a/src/components/DappBrowser/DappBrowser.tsx +++ b/src/components/DappBrowser/DappBrowser.tsx @@ -1,13 +1,13 @@ import React, { useEffect, useState } from 'react'; import { StyleSheet } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; -import Animated, { interpolateColor, runOnJS, useAnimatedProps, useAnimatedReaction, useAnimatedStyle } from 'react-native-reanimated'; +import Animated, { interpolateColor, useAnimatedProps, useAnimatedReaction, useAnimatedStyle, withTiming } from 'react-native-reanimated'; import RNFS from 'react-native-fs'; import { Page } from '@/components/layout'; import { Box, globalColors, useColorMode } from '@/design-system'; import { IS_ANDROID } from '@/env'; -import { safeAreaInsetValues } from '@/utils'; +import { deviceUtils, safeAreaInsetValues } from '@/utils'; import { BrowserContextProvider, useBrowserContext } from './BrowserContext'; import { BrowserTab, pruneScreenshots } from './BrowserTab'; import { TAB_VIEW_ROW_HEIGHT } from './Dimensions'; @@ -16,6 +16,7 @@ import { TabViewToolbar } from './TabViewToolbar'; import { SheetGestureBlocker } from '../sheet/SheetGestureBlocker'; import { ProgressBar } from './ProgressBar'; import { RouteProp, useRoute } from '@react-navigation/native'; +import { TIMING_CONFIGS } from '../animations/animationConfigs'; const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); @@ -85,6 +86,20 @@ const DappBrowserComponent = () => { }; }); + const scrollViewHeightStyle = useAnimatedStyle(() => { + const height = Math.max( + Math.ceil((currentlyOpenTabIds?.value.length || 0) / 2) * TAB_VIEW_ROW_HEIGHT + + safeAreaInsetValues.bottom + + 165 + + 28 + + (IS_ANDROID ? 35 : 0), + deviceUtils.dimensions.height + ); + // Using paddingBottom on a nested container instead of height because the height of the ScrollView + // seemingly cannot be directly animated. This works because the tabs are all positioned absolutely. + return { paddingBottom: withTiming(height, TIMING_CONFIGS.tabPressConfig) }; + }); + const scrollEnabledProp = useAnimatedProps(() => ({ scrollEnabled: tabViewVisible?.value, })); @@ -99,24 +114,22 @@ const DappBrowserComponent = () => { style={[ backgroundStyle, { - paddingTop: android ? 30 : 0, + paddingTop: IS_ANDROID ? 30 : 0, }, ]} width="full" /> - {tabStates.map((_, index) => ( - - ))} + + {tabStates.map((_, index) => ( + + ))} + From 397ec6a369cef9d5c59b5b44c458c602870a8c2f Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:34:06 -0400 Subject: [PATCH 12/18] Handle tab view toggling internally when creating a new tab --- src/components/DappBrowser/BrowserContext.tsx | 2 +- src/components/DappBrowser/DappBrowser.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/DappBrowser/BrowserContext.tsx b/src/components/DappBrowser/BrowserContext.tsx index d7d9bc77f93..9286f42dd43 100644 --- a/src/components/DappBrowser/BrowserContext.tsx +++ b/src/components/DappBrowser/BrowserContext.tsx @@ -315,7 +315,7 @@ export const BrowserContextProvider = ({ children }: { children: React.ReactNode url: operation.newTabUrl || RAINBOW_HOME, }; newTabStates.push(newTab); - shouldToggleTabView = true; + if (tabViewVisible?.value) shouldToggleTabView = true; newActiveIndex = indexForNewTab; } else { // ⚠️ TODO: Add logging here to report any time a new tab operation is given a nonexistent diff --git a/src/components/DappBrowser/DappBrowser.tsx b/src/components/DappBrowser/DappBrowser.tsx index cbd2cf77ecc..5abe2dd2f9c 100644 --- a/src/components/DappBrowser/DappBrowser.tsx +++ b/src/components/DappBrowser/DappBrowser.tsx @@ -42,7 +42,7 @@ const DappBrowserComponent = () => { const { isDarkMode } = useColorMode(); const [injectedJS, setInjectedJS] = useState(''); - const { scrollViewRef, tabStates, tabViewProgress, tabViewVisible, newTabWorklet, toggleTabViewWorklet } = useBrowserContext(); + const { currentlyOpenTabIds, newTabWorklet, scrollViewRef, tabStates, tabViewProgress, tabViewVisible } = useBrowserContext(); const route = useRoute>(); @@ -51,7 +51,6 @@ const DappBrowserComponent = () => { (current, previous) => { if (current !== previous && route.params?.url) { newTabWorklet(current); - toggleTabViewWorklet(); } }, [newTabWorklet, route.params?.url] From 5c9f487c7a9ff4ad9dfb48b207bb8199b768bd96 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 03:04:04 -0400 Subject: [PATCH 13/18] Fix close tab button hit box, use animatedTabIndex for tab closing --- src/components/DappBrowser/BrowserTab.tsx | 9 ++-- src/components/DappBrowser/CloseTabButton.tsx | 46 +++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index ceb07d10e22..9787e5953b6 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -662,7 +662,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje if (shouldDismiss) { 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; + const storedTabIndex = animatedTabIndex.value; // Remove the tab from currentlyOpenTabIds as soon as the swipe-to-close gesture is confirmed currentlyOpenTabIds?.modify(value => { const index = value.indexOf(tabId); @@ -678,8 +678,8 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje 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. + // In the event two tabs are open when this one is closed, we animate its Y position to align it + // vertically with the 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) { @@ -849,12 +849,13 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje diff --git a/src/components/DappBrowser/CloseTabButton.tsx b/src/components/DappBrowser/CloseTabButton.tsx index 4fecbfd3f2d..9a7143c6a95 100644 --- a/src/components/DappBrowser/CloseTabButton.tsx +++ b/src/components/DappBrowser/CloseTabButton.tsx @@ -27,34 +27,36 @@ const SCALE_ADJUSTED_X_BUTTON_PADDING_SINGLE_TAB = X_BUTTON_PADDING * SINGLE_TAB export const CloseTabButton = ({ animatedMultipleTabsOpen, + animatedTabIndex, + gestureScale, gestureX, gestureY, isOnHomepage, multipleTabsOpen, tabId, - tabIndex, }: { animatedMultipleTabsOpen: SharedValue; + animatedTabIndex: SharedValue; + gestureScale: SharedValue; gestureX: SharedValue; gestureY: 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 rawAnimatedTabIndex = currentlyOpenTabIds?.value.indexOf(tabId); - const animatedTabIndex = rawAnimatedTabIndex === -1 ? tabIndex : rawAnimatedTabIndex ?? tabIndex; - const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex; + const animatedIsActiveTab = animatedActiveTabIndex?.value === animatedTabIndex.value; + // 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], [animatedIsActiveTab ? 0 : 1, animatedIsActiveTab ? 0 : 1, 1]); const opacity = tabViewVisible?.value || !animatedIsActiveTab ? interpolatedOpacity : withTiming(0, TIMING_CONFIGS.fastFadeConfig); + return { opacity }; }); @@ -67,12 +69,12 @@ export const CloseTabButton = ({ const pointerEvents = tabViewVisible?.value && !isEmptyState ? 'auto' : 'none'; return { - height: buttonSize, + height: buttonSize + buttonPadding * 2, opacity, pointerEvents, - right: buttonPadding, - top: buttonPadding, - width: buttonSize, + right: 0, + top: 0, + width: buttonSize + buttonPadding * 2, }; }); @@ -92,12 +94,14 @@ export const CloseTabButton = ({ const closeTab = useCallback(() => { 'worklet'; - const storedTabIndex = currentlyOpenTabIds?.value.indexOf(tabId) ?? tabIndex; + // Store the tab's index before modifying currentlyOpenTabIds, so we can pass it along to closeTabWorklet() + const storedTabIndex = animatedTabIndex.value; const isOnlyOneTabOpen = (currentlyOpenTabIds?.value.length || 0) === 1; const isTabInLeftColumn = storedTabIndex % 2 === 0 && !isOnlyOneTabOpen; const xDestination = isTabInLeftColumn ? -deviceUtils.dimensions.width / 1.5 : -deviceUtils.dimensions.width; + // Remove the tab from currentlyOpenTabIds as soon as the tab close is initiated currentlyOpenTabIds?.modify(value => { const index = value.indexOf(tabId); if (index !== -1) { @@ -105,24 +109,32 @@ export const CloseTabButton = ({ } return value; }); + gestureX.value = withTiming(xDestination, TIMING_CONFIGS.tabPressConfig, () => { + // Ensure the tab remains hidden after being swiped off screen (until the tab is destroyed) + gestureScale.value = 0; // 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. + // In the event two tabs are open when this one is closed, we animate its Y position to align it + // vertically with the 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]); + }, [animatedTabIndex, closeTabWorklet, currentlyOpenTabIds, gestureScale, gestureX, gestureY, tabId]); return ( - + {IS_IOS ? ( Date: Thu, 11 Apr 2024 03:16:17 -0400 Subject: [PATCH 14/18] faster-image tweaks, misc. cleanup --- src/components/DappBrowser/BrowserTab.tsx | 14 ++++++-------- src/components/DappBrowser/constants.ts | 2 +- src/components/images/ImgixImage.tsx | 12 +++++++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index 9787e5953b6..b15bebe9d0a 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -46,14 +46,14 @@ import RNFS from 'react-native-fs'; import { WebViewEvent } from 'react-native-webview/lib/WebViewTypes'; import { appMessenger } from '@/browserMessaging/AppMessenger'; import { IS_ANDROID, IS_DEV, IS_IOS } from '@/env'; +import { RainbowError, logger } from '@/logger'; import { CloseTabButton, X_BUTTON_PADDING, X_BUTTON_SIZE } from './CloseTabButton'; import DappBrowserWebview from './DappBrowserWebview'; import Homepage from './Homepage'; import { handleProviderRequestApp } from './handleProviderRequest'; import { WebViewBorder } from './WebViewBorder'; import { SPRING_CONFIGS, TIMING_CONFIGS } from '../animations/animationConfigs'; -import { RainbowError, logger } from '@/logger'; -import { FASTER_IMAGE_CONFIG, RAINBOW_HOME } from './constants'; +import { TAB_SCREENSHOT_FASTER_IMAGE_CONFIG, RAINBOW_HOME } from './constants'; import { getWebsiteMetadata } from './scripts'; // ⚠️ TODO: Split this file apart into hooks, smaller components @@ -335,11 +335,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) }, @@ -443,7 +443,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje if (webViewRef.current !== null && isActiveTab) { activeTabRef.current = webViewRef.current; if (title.current) { - // @ts-expect-error + // @ts-expect-error Property 'title' does not exist on type 'WebView<{}>' activeTabRef.current.title = title.current; } } @@ -510,7 +510,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje const screenshotSource = useDerivedValue(() => { return { - ...FASTER_IMAGE_CONFIG, + ...TAB_SCREENSHOT_FASTER_IMAGE_CONFIG, url: screenshotData.value?.uri ? `file://${screenshotData.value?.uri}` : '', } as ImageOptions; }); @@ -740,8 +740,6 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje isScrollViewScrollable && tabViewVisible?.value === false && current === 0 && previous && previous !== 0; if (exitTabViewAnimationIsComplete && isScrollViewScrollable) { - consoleLogWorklet('SCROLLING TAB INTO POSITION'); - const currentTabRow = Math.floor(animatedTabIndex.value / 2); const scrollViewHeight = Math.ceil((currentlyOpenTabIds?.value.length || 0) / 2) * TAB_VIEW_ROW_HEIGHT + @@ -825,7 +823,7 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje mediaPlaybackRequiresUserAction onLoadStart={handleOnLoadStart} onLoad={handleOnLoad} - // 👇 This prevents the WebView from hiding its content on load/reload + // 👇 This eliminates a white flash and prevents the WebView from hiding its content on load/reload renderLoading={() => <>} onLoadEnd={handleOnLoadEnd} onError={handleOnError} diff --git a/src/components/DappBrowser/constants.ts b/src/components/DappBrowser/constants.ts index 3c807c747fc..6e46d51d081 100644 --- a/src/components/DappBrowser/constants.ts +++ b/src/components/DappBrowser/constants.ts @@ -7,7 +7,7 @@ export const RAINBOW_HOME = 'RAINBOW_HOME'; const BLANK_BASE64_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; -export const FASTER_IMAGE_CONFIG: Partial = { +export const TAB_SCREENSHOT_FASTER_IMAGE_CONFIG: Partial = { // This placeholder avoids an occasional loading spinner flash base64Placeholder: BLANK_BASE64_PIXEL, cachePolicy: 'discNoCacheControl', diff --git a/src/components/images/ImgixImage.tsx b/src/components/images/ImgixImage.tsx index d737984e55d..1cb0694a813 100644 --- a/src/components/images/ImgixImage.tsx +++ b/src/components/images/ImgixImage.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; import FastImage, { FastImageProps, Source } from 'react-native-fast-image'; import { maybeSignSource } from '../../handlers/imgix'; -import { FASTER_IMAGE_CONFIG } from '../DappBrowser/constants'; +import { TAB_SCREENSHOT_FASTER_IMAGE_CONFIG } from '../DappBrowser/constants'; export type ImgixImageProps = FastImageProps & { readonly Component?: React.ElementType; @@ -42,7 +42,7 @@ class ImgixImage extends React.PureComponent Date: Thu, 11 Apr 2024 03:16:48 -0400 Subject: [PATCH 15/18] Improve screenshot display logic --- src/components/DappBrowser/BrowserTab.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/DappBrowser/BrowserTab.tsx b/src/components/DappBrowser/BrowserTab.tsx index b15bebe9d0a..de0c293001d 100644 --- a/src/components/DappBrowser/BrowserTab.tsx +++ b/src/components/DappBrowser/BrowserTab.tsx @@ -516,7 +516,11 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje }); const animatedScreenshotStyle = useAnimatedStyle(() => { - const animatedIsActiveTab = animatedActiveTabIndex?.value === tabIndex; + // Note: We use isActiveTab throughout this animated style over animatedIsActiveTab + // because the displaying of the screenshot should be synced to the WebView freeze + // state, which is driven by the slower JS-side isActiveTab. This prevents the + // screenshot from disappearing before the WebView is unfrozen. + const screenshotExists = !!screenshotData.value?.uri; const screenshotMatchesTabIdAndUrl = screenshotData.value?.id === tabId && screenshotData.value?.url === tabStates[tabIndex].url; @@ -525,17 +529,16 @@ export const BrowserTab = React.memo(function BrowserTab({ tabId, tabIndex, inje // it doesn't unfreeze immediately, so this condition allows some time for the tab to // become unfrozen before the screenshot is hidden, in most cases hiding the flash of // the frozen empty WebView that occurs if the screenshot is hidden immediately. - const isActiveTabButMaybeStillFrozen = animatedIsActiveTab && (tabViewProgress?.value || 0) > 50 && !tabViewVisible?.value; + const isActiveTabButMaybeStillFrozen = isActiveTab && (tabViewProgress?.value || 0) > 75 && !tabViewVisible?.value; const oneMinuteAgo = Date.now() - 1000 * 60; - const isScreenshotStale = screenshotData.value && screenshotData.value?.timestamp < oneMinuteAgo; - const shouldWaitForNewScreenshot = - isScreenshotStale && animatedIsActiveTab && !!tabViewVisible?.value && !isActiveTabButMaybeStillFrozen; + const isScreenshotStale = !!(screenshotData.value && screenshotData.value?.timestamp < oneMinuteAgo); + const shouldWaitForNewScreenshot = isScreenshotStale && !!tabViewVisible?.value && isActiveTab && !isActiveTabButMaybeStillFrozen; const shouldDisplay = screenshotExists && screenshotMatchesTabIdAndUrl && - (!animatedIsActiveTab || !!tabViewVisible?.value || isActiveTabButMaybeStillFrozen) && + (!isActiveTab || !!tabViewVisible?.value || isActiveTabButMaybeStillFrozen) && !shouldWaitForNewScreenshot; return { From dd63d7931239231171358bdd16e5548a1cae8ee1 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 03:31:47 -0400 Subject: [PATCH 16/18] Homepage UI improvements, adjust placeholders --- src/components/DappBrowser/Homepage.tsx | 250 ++++++++++--------- src/resources/trendingDapps/trendingDapps.ts | 42 ++-- 2 files changed, 157 insertions(+), 135 deletions(-) diff --git a/src/components/DappBrowser/Homepage.tsx b/src/components/DappBrowser/Homepage.tsx index 33c207a3be2..0f5a61ba6d1 100644 --- a/src/components/DappBrowser/Homepage.tsx +++ b/src/components/DappBrowser/Homepage.tsx @@ -1,9 +1,9 @@ +import React from 'react'; import { ButtonPressAnimation } from '@/components/animations'; import { Page } from '@/components/layout'; import { Bleed, Box, ColorModeProvider, Cover, Inline, Inset, Stack, Text, TextIcon, globalColors, useColorMode } from '@/design-system'; import { deviceUtils } from '@/utils'; -import React, { useCallback } from 'react'; -import { ScrollView, View } from 'react-native'; +import { ScrollView, StyleSheet, View } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import { BlurView } from '@react-native-community/blur'; import { ImgixImage } from '@/components/images'; @@ -18,7 +18,6 @@ import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; import MaskedView from '@react-native-masked-view/masked-view'; import { useBrowserContext } from './BrowserContext'; import { GestureHandlerV1Button } from '@/__swaps__/screens/Swap/components/GestureHandlerV1Button'; -import { isEmpty } from 'lodash'; import { normalizeUrl } from './utils'; const HORIZONTAL_PAGE_INSET = 24; @@ -34,8 +33,8 @@ const CARD_PADDING = 12; const CARD_SIZE = (deviceUtils.dimensions.width - HORIZONTAL_PAGE_INSET * 2 - (NUM_CARDS - 1) * CARD_PADDING) / NUM_CARDS; const Card = ({ site, showMenuButton }: { showMenuButton?: boolean; site: TrendingSite }) => { - const { isDarkMode } = useColorMode(); const { updateActiveTabState } = useBrowserContext(); + const { isDarkMode } = useColorMode(); const menuConfig = { menuTitle: '', @@ -60,123 +59,122 @@ const Card = ({ site, showMenuButton }: { showMenuButton?: boolean; site: Trendi }; return ( - updateActiveTabState({ url: normalizeUrl(site.url) })} scaleTo={0.9}> - + + updateActiveTabState({ url: normalizeUrl(site.url) })} scaleTo={0.94}> - - {site.screenshot && ( - + + + {site.screenshot && ( + + + + + + + )} + - - + + + {site.name} + + + {site.url} + + + + + {IS_IOS && ( + + )} + + + {showMenuButton && ( + {}} style={styles.cardContextMenuButton}> + + + + {IS_IOS ? ( + - + ) : ( + + )} - )} - - - - - - {site.name} - - - {site.url} - - - {showMenuButton && ( - {}} - style={{ top: 12, right: 12, height: 24, width: 24, position: 'absolute' }} > - - - - {IS_IOS ? ( - - ) : ( - - )} - - - - 􀍠 - - - - - - )} - - - {IS_IOS && ( - - )} - - + + 􀍠 + + + + + + )} + ); }; @@ -189,7 +187,7 @@ const Logo = ({ site }: { site: Omit }) => { updateActiveTabState({ url: normalizeUrl(site.url) })}> - {IS_IOS && !isEmpty(site.image) && ( + {IS_IOS && !site.image && ( }) => { @@ -288,7 +286,13 @@ export default function Homepage() { - + index * (CARD_SIZE + CARD_PADDING))} + > {trendingDapps.map(site => ( @@ -316,7 +320,7 @@ export default function Homepage() { width={{ custom: deviceUtils.dimensions.width - HORIZONTAL_PAGE_INSET * 2 }} > {favoriteDapps.map(dapp => ( - + ))} @@ -331,7 +335,7 @@ export default function Homepage() { - {trendingDapps.map(site => ( + {[trendingDapps[trendingDapps.length - 4], trendingDapps[trendingDapps.length - 2]].map(site => ( ))} @@ -341,3 +345,15 @@ export default function Homepage() { ); } + +const styles = StyleSheet.create({ + cardContextMenuButton: { + alignItems: 'center', + top: 0, + right: 0, + height: 48, + justifyContent: 'center', + width: 48, + position: 'absolute', + }, +}); diff --git a/src/resources/trendingDapps/trendingDapps.ts b/src/resources/trendingDapps/trendingDapps.ts index 5dbddd07909..f55310d4798 100644 --- a/src/resources/trendingDapps/trendingDapps.ts +++ b/src/resources/trendingDapps/trendingDapps.ts @@ -6,6 +6,30 @@ export type TrendingSite = { }; export const trendingDapps: TrendingSite[] = [ + { + name: 'Zora', + url: 'zora.co', + image: 'https://rainbowme-res.cloudinary.com/image/upload/v1692144154/dapps/ingested_zora.co.png', + screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1692144154/dapps/ingested_zora.co.png', + }, + { + name: 'Party', + url: 'party.app', + image: 'https://rainbowme-res.cloudinary.com/image/upload/v1694735726/dapps/Party.app.png', + screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1694735726/dapps/Party.app.png', + }, + { + name: 'BasePaint', + url: 'basepaint.xyz', + image: 'https://pbs.twimg.com/profile_images/1704878803611447296/6HniqSfx_400x400.jpg', + screenshot: 'https://i.seadn.io/s/raw/files/7c38b0c9b5903d08817102466e9fff27.png?auto=format&dpr=1&w=600', + }, + { + name: 'Uniswap', + url: 'app.uniswap.org', + image: 'https://rainbowme-res.cloudinary.com/image/upload/v1668565116/dapps/uniswap.org.png', + screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1668565116/dapps/uniswap.org.png', + }, { name: 'Aave', url: 'app.aave.com', @@ -24,22 +48,4 @@ export const trendingDapps: TrendingSite[] = [ image: 'https://rainbowme-res.cloudinary.com/image/upload/v1688068601/dapps/ingested_opensea.io.png', screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1688068601/dapps/ingested_opensea.io.png', }, - { - name: 'Party', - url: 'party.app', - image: 'https://rainbowme-res.cloudinary.com/image/upload/v1694735726/dapps/Party.app.png', - screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1694735726/dapps/Party.app.png', - }, - { - name: 'Uniswap', - url: 'app.uniswap.org', - image: 'https://rainbowme-res.cloudinary.com/image/upload/v1668565116/dapps/uniswap.org.png', - screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1668565116/dapps/uniswap.org.png', - }, - { - name: 'Zora', - url: 'zora.co', - image: 'https://rainbowme-res.cloudinary.com/image/upload/v1692144154/dapps/ingested_zora.co.png', - screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1692144154/dapps/ingested_zora.co.png', - }, ]; From 9b815f8d859dbf3cbf65c5480d4b1bf2020da33e Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 04:05:31 -0400 Subject: [PATCH 17/18] Prevent crash --- src/state/appSessions/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state/appSessions/index.ts b/src/state/appSessions/index.ts index 9c9f8c4c4a6..9330b0d6c27 100644 --- a/src/state/appSessions/index.ts +++ b/src/state/appSessions/index.ts @@ -43,7 +43,7 @@ export const appSessionsStore = createStore>( const appSessions = get().appSessions; const activeSessionAddress = appSessions[host]?.activeSessionAddress; const sessions = appSessions[host]?.sessions; - return activeSessionAddress + return activeSessionAddress && sessions ? { address: activeSessionAddress, network: sessions[activeSessionAddress], From 83ae3118168e4169ba043b7fd922c17059b40fd1 Mon Sep 17 00:00:00 2001 From: Christian Baroni <7061887+christianbaroni@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:28:23 -0400 Subject: [PATCH 18/18] Clean up placeholder dapps --- src/components/DappBrowser/Homepage.tsx | 4 +-- src/resources/trendingDapps/trendingDapps.ts | 35 +++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/components/DappBrowser/Homepage.tsx b/src/components/DappBrowser/Homepage.tsx index 0f5a61ba6d1..4f821df38b1 100644 --- a/src/components/DappBrowser/Homepage.tsx +++ b/src/components/DappBrowser/Homepage.tsx @@ -13,7 +13,7 @@ import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; import { opacity } from '@/__swaps__/screens/Swap/utils/swaps'; import { Site } from '@/state/browserState'; import { useFavoriteDappsStore } from '@/state/favoriteDapps'; -import { TrendingSite, trendingDapps } from '@/resources/trendingDapps/trendingDapps'; +import { TrendingSite, recentDapps, trendingDapps } from '@/resources/trendingDapps/trendingDapps'; import { FadeMask } from '@/__swaps__/screens/Swap/components/FadeMask'; import MaskedView from '@react-native-masked-view/masked-view'; import { useBrowserContext } from './BrowserContext'; @@ -335,7 +335,7 @@ export default function Homepage() { - {[trendingDapps[trendingDapps.length - 4], trendingDapps[trendingDapps.length - 2]].map(site => ( + {recentDapps.map(site => ( ))} diff --git a/src/resources/trendingDapps/trendingDapps.ts b/src/resources/trendingDapps/trendingDapps.ts index f55310d4798..699b7d0c1f9 100644 --- a/src/resources/trendingDapps/trendingDapps.ts +++ b/src/resources/trendingDapps/trendingDapps.ts @@ -18,6 +18,12 @@ export const trendingDapps: TrendingSite[] = [ image: 'https://rainbowme-res.cloudinary.com/image/upload/v1694735726/dapps/Party.app.png', screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1694735726/dapps/Party.app.png', }, + { + name: 'OpenSea', + url: 'opensea.io', + image: 'https://pbs.twimg.com/profile_images/1760855147662458880/COJ9xiFz_400x400.png', + screenshot: 'https://pbs.twimg.com/profile_banners/946213559213555712/1708655559/1500x500', + }, { name: 'BasePaint', url: 'basepaint.xyz', @@ -42,10 +48,31 @@ export const trendingDapps: TrendingSite[] = [ image: 'https://rainbowme-res.cloudinary.com/image/upload/v1694734011/dapps/mirror.xyz.jpg', screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1694734011/dapps/mirror.xyz.jpg', }, +]; + +export const recentDapps: TrendingSite[] = [ { - name: 'Opensea', - url: 'opensea.io', - image: 'https://rainbowme-res.cloudinary.com/image/upload/v1688068601/dapps/ingested_opensea.io.png', - screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1688068601/dapps/ingested_opensea.io.png', + name: 'BasePaint', + url: 'basepaint.xyz', + image: 'https://pbs.twimg.com/profile_images/1704878803611447296/6HniqSfx_400x400.jpg', + screenshot: 'https://i.seadn.io/s/raw/files/7c38b0c9b5903d08817102466e9fff27.png?auto=format&dpr=1&w=600', + }, + { + name: 'Blur', + url: 'blur.io', + image: 'https://pbs.twimg.com/profile_images/1518705644450291713/X2FLVDdn_400x400.jpg', + screenshot: 'https://pbs.twimg.com/profile_banners/1481340056426143744/1646076761/1500x500', + }, + { + name: 'Uniswap', + url: 'app.uniswap.org', + image: 'https://rainbowme-res.cloudinary.com/image/upload/v1668565116/dapps/uniswap.org.png', + screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1668565116/dapps/uniswap.org.png', + }, + { + name: 'Mirror', + url: 'mirror.xyz', + image: 'https://rainbowme-res.cloudinary.com/image/upload/v1694734011/dapps/mirror.xyz.jpg', + screenshot: 'https://rainbowme-res.cloudinary.com/image/upload/v1694734011/dapps/mirror.xyz.jpg', }, ];