Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
796 changes: 475 additions & 321 deletions mobile/app/DAppBrowser.tsx

Large diffs are not rendered by default.

99 changes: 84 additions & 15 deletions mobile/app/DAppBrowserTabItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ interface BrowserTab {
interface DAppBrowserTabItemProps {
tab: BrowserTab;
index: number;
isVisible: boolean;
onPress: () => void;
onClose: () => void;
getTabTitle: (url: string) => string;
onEnsurePreview: (forceReload?: boolean) => void;
onInvalidatePreview?: () => void;
}

export const DAppBrowserTabItem: React.FC<DAppBrowserTabItemProps> = ({ tab, index, onPress, onClose, onEnsurePreview }) => {
export const DAppBrowserTabItem: React.FC<DAppBrowserTabItemProps> = ({ tab, index, isVisible, onPress, onClose, onEnsurePreview, onInvalidatePreview }) => {
const [imageError, setImageError] = useState(false);
const [hasLoaded, setHasLoaded] = useState(false);
const [reloadToken, setReloadToken] = useState(0);

const getDomainName = (url: string): string => {
try {
Expand All @@ -38,13 +42,33 @@ export const DAppBrowserTabItem: React.FC<DAppBrowserTabItemProps> = ({ tab, ind
}
};

const getFaviconUrl = (url: string): string | null => {
try {
const domain = getDomainName(url);
if (!domain) {
return null;
}
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(domain)}&sz=64`;
} catch {
return null;
}
};

useEffect(() => {
// Reset error state when screenshot changes
if (tab.screenshot) {
setImageError(false);
setHasLoaded(false);
setReloadToken((v) => v + 1);
}
}, [tab.screenshot]);

useEffect(() => {
if (isVisible) {
setImageError(false);
}
}, [isVisible]);

useEffect(() => {
// Ensure preview is loaded for tabs without screenshots
if (!tab.screenshot) {
Expand All @@ -53,27 +77,45 @@ export const DAppBrowserTabItem: React.FC<DAppBrowserTabItemProps> = ({ tab, ind
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tab.screenshot, tab.id]);

useEffect(() => {
if (!tab.screenshot || imageError || hasLoaded) return;
const timeout = setTimeout(() => {
if (!hasLoaded) {
setReloadToken((v) => v + 1);
}
}, 1500);
return () => clearTimeout(timeout);
}, [tab.screenshot, imageError, hasLoaded]);

return (
<Pressable style={styles.tabCard} onPress={onPress}>
<View style={styles.tabCardHeader}>
<View style={styles.tabCardTitleContainer}>
<ThemedText style={styles.tabCardNumber}>#{index + 1}</ThemedText>
<ThemedText style={styles.tabCardTitle} numberOfLines={1}>
{tab.title}
</ThemedText>
<View style={styles.faviconWrapper}>
{getFaviconUrl(tab.url) ? (
<Image source={{ uri: getFaviconUrl(tab.url) as string }} style={styles.favicon} />
) : (
<View style={styles.faviconFallback}>
<ThemedText style={styles.faviconFallbackText}>{getDomainName(tab.url).charAt(0).toUpperCase()}</ThemedText>
</View>
)}
</View>
</View>
<Pressable style={styles.tabCardCloseButton} onPress={onClose}>
<Pressable style={styles.tabCardCloseButton} onPress={onClose} hitSlop={8}>
<Ionicons name="close" size={16} color="rgba(255, 255, 255, 0.8)" />
</Pressable>
</View>

<View style={styles.tabCardPreview}>
{tab.screenshot && !imageError ? (
<Image
key={tab.screenshot}
key={`${tab.screenshot}-${tab.timestamp}-${reloadToken}`}
source={{ uri: tab.screenshot }}
style={styles.tabCardScreenshot}
resizeMode="cover"
onLoad={() => {
setHasLoaded(true);
}}
onError={() => {
setImageError(true);
// Try to reload from storage if the screenshot URI is invalid
Expand All @@ -87,7 +129,7 @@ export const DAppBrowserTabItem: React.FC<DAppBrowserTabItemProps> = ({ tab, ind
)}
<View style={styles.tabCardUrlOverlay}>
<ThemedText style={styles.tabCardUrlText} numberOfLines={1} ellipsizeMode="middle">
{tab.url || 'Untitled Tab'}
{tab.title || getDomainName(tab.url) || 'Untitled Tab'}
</ThemedText>
</View>
</View>
Expand Down Expand Up @@ -117,20 +159,47 @@ const styles = StyleSheet.create({
flex: 1,
marginRight: 8,
},
tabCardNumber: {
fontSize: 12,
fontWeight: '700',
color: 'rgba(255, 255, 255, 0.6)',
marginRight: 6,
},
tabCardTitle: {
fontSize: 14,
fontWeight: '600',
color: 'white',
flex: 1,
},
faviconWrapper: {
width: 18,
height: 18,
borderRadius: 9,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
},
favicon: {
width: 18,
height: 18,
borderRadius: 9,
},
faviconFallback: {
width: 18,
height: 18,
borderRadius: 9,
backgroundColor: 'rgba(255, 255, 255, 0.16)',
alignItems: 'center',
justifyContent: 'center',
},
faviconFallbackText: {
fontSize: 10,
color: 'rgba(255, 255, 255, 0.9)',
fontWeight: '600',
},
tabCardCloseButton: {
padding: 4,
width: 22,
height: 22,
borderRadius: 11,
backgroundColor: 'rgba(255, 255, 255, 0.14)',
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'rgba(255, 255, 255, 0.18)',
alignItems: 'center',
justifyContent: 'center',
},
tabCardPreview: {
flex: 1,
Expand Down
65 changes: 60 additions & 5 deletions mobile/app/DAppBrowserTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React from 'react';
import { View, ScrollView, StyleSheet } from 'react-native';
import Animated from 'react-native-reanimated';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { ThemedText } from '@/components/ThemedText';
import { DAppBrowserTabItem } from './DAppBrowserTabItem';
import Pressable from '../components/Pressable';

interface BrowserTab {
id: string;
Expand All @@ -22,23 +26,46 @@ interface DAppBrowserTabsProps {
activeTabId: string;
animatedStyle: any;
pointerEvents: 'auto' | 'none';
isVisible: boolean;
onSwitchTab: (tabId: string) => void;
onCloseTab: (tabId: string) => void;
getTabTitle: (url: string) => string;
onEnsurePreview: (tabId: string, forceReload?: boolean) => void | Promise<void>;
onInvalidatePreview: (tabId: string) => void;
onCloseOverview: () => void;
}

export const DAppBrowserTabs: React.FC<DAppBrowserTabsProps> = ({ tabs, animatedStyle, pointerEvents, onSwitchTab, onCloseTab, getTabTitle, onEnsurePreview }) => {
export const DAppBrowserTabs: React.FC<DAppBrowserTabsProps> = ({
tabs,
animatedStyle,
pointerEvents,
isVisible,
onSwitchTab,
onCloseTab,
getTabTitle,
onEnsurePreview,
onInvalidatePreview,
onCloseOverview,
}) => {
const insets = useSafeAreaInsets();

return (
<Animated.View style={[styles.tabsOverviewContainer, animatedStyle, styles.tabsOverviewAbsolute]} pointerEvents={pointerEvents}>
<View style={styles.tabsOverviewBackground}>
<ScrollView style={styles.tabsOverviewContent} contentContainerStyle={styles.tabsGridContainer}>
<SafeAreaView style={styles.tabsOverviewBackground} edges={['top', 'left', 'right']}>
<View style={styles.header}>
<ThemedText style={styles.title}>Tabs</ThemedText>
<Pressable style={styles.headerMenuButton} onPress={onCloseOverview} testID="BrowserTabsCloseOverviewButton">
<Ionicons name="close" size={22} color="rgba(255, 255, 255, 0.9)" />
</Pressable>
</View>
<ScrollView style={styles.tabsOverviewContent} contentContainerStyle={[styles.tabsGridContainer, { paddingBottom: insets.bottom + 140 }]}>
<View style={styles.tabsGrid}>
{tabs.map((tab, index) => (
<DAppBrowserTabItem
key={`tab-card-${tab.id}`}
tab={tab}
index={index}
isVisible={isVisible}
onPress={() => {
onSwitchTab(tab.id);
}}
Expand All @@ -49,11 +76,14 @@ export const DAppBrowserTabs: React.FC<DAppBrowserTabsProps> = ({ tabs, animated
onEnsurePreview={(forceReload) => {
void onEnsurePreview(tab.id, forceReload);
}}
onInvalidatePreview={() => {
onInvalidatePreview(tab.id);
}}
/>
))}
</View>
</ScrollView>
</View>
</SafeAreaView>
</Animated.View>
);
};
Expand All @@ -73,10 +103,35 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.85)',
},
header: {
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 8,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
title: {
fontSize: 34,
fontWeight: '700',
lineHeight: 42,
color: 'rgba(255, 255, 255, 0.95)',
letterSpacing: -0.6,
},
headerMenuButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: 'rgba(255, 255, 255, 0.12)',
borderWidth: StyleSheet.hairlineWidth,
borderColor: 'rgba(255, 255, 255, 0.2)',
alignItems: 'center',
justifyContent: 'center',
},
tabsOverviewContent: {
flex: 1,
paddingHorizontal: 20,
paddingTop: 100,
paddingTop: 8,
},
tabsGridContainer: {
paddingBottom: 20,
Expand Down
3 changes: 3 additions & 0 deletions mobile/assets/images/home.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading