Skip to content

Commit 83a9bce

Browse files
authored
Merge pull request #10 from afnx/feature/browser-navigation
Feature/browser navigation
2 parents a7f185e + 85fc215 commit 83a9bce

4 files changed

Lines changed: 148 additions & 92 deletions

File tree

src/features/browser/screens/BrowserScreen/AddressBar.tsx

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@ import { formatUrlForDisplay } from "../../utils/urlUtils";
88

99
interface AddressBarProps {
1010
onFocusChange?: (isFocused: boolean) => void;
11+
onReload: () => void;
12+
onStop: () => void;
1113
}
1214

13-
export default function AddressBar({ onFocusChange }: AddressBarProps) {
15+
export default function AddressBar({
16+
onFocusChange,
17+
onReload,
18+
onStop,
19+
}: AddressBarProps) {
1420
const theme = useTheme();
1521
const { activeTab, navigateTab, activeTabId } = useBrowser();
1622

@@ -74,13 +80,21 @@ export default function AddressBar({ onFocusChange }: AddressBarProps) {
7480
/>
7581
<View style={[styles.iconRight]}>
7682
<IconButton
77-
icon={activeTab?.url && !isFocused ? Icons.refresh : Icons.microphone}
83+
icon={
84+
activeTab?.isLoading
85+
? Icons.stop
86+
: activeTab?.url && !isFocused
87+
? Icons.refresh
88+
: Icons.microphone
89+
}
7890
color={theme.colors.textSecondary}
7991
size={26}
8092
accessibilityLabel={activeTab?.url ? "Reload" : "Start voice search"}
8193
onPress={() => {
82-
if (activeTabId && activeTab?.url) {
83-
navigateTab(activeTabId, activeTab.url);
94+
if (activeTab?.isLoading) {
95+
onStop();
96+
} else if (activeTabId && activeTab?.url) {
97+
onReload();
8498
}
8599
}}
86100
/>

src/features/browser/screens/BrowserScreen/Toolbar.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ import { StyleSheet } from "react-native";
66
import { SafeAreaView } from "react-native-safe-area-context";
77
import { useBrowser } from "../../hooks/useBrowser";
88

9-
export default function Toolbar() {
9+
type ToolbarProps = {
10+
onBack: () => void;
11+
onForward: () => void;
12+
onShare?: () => void;
13+
};
14+
15+
export default function Toolbar({ onBack, onForward, onShare }: ToolbarProps) {
1016
const theme = useTheme();
1117
const {
1218
activeTab,
@@ -25,28 +31,22 @@ export default function Toolbar() {
2531
size={26}
2632
disabled={!activeTab?.canGoBack}
2733
accessibilityLabel="Back"
28-
onPress={() => {
29-
// WebView navigation logic will go here
30-
}}
34+
onPress={onBack}
3135
/>
3236
<IconButton
3337
icon={Icons.forward}
3438
color={theme.colors.textPrimary}
3539
size={26}
3640
disabled={!activeTab?.canGoForward}
3741
accessibilityLabel="Forward"
38-
onPress={() => {
39-
// WebView navigation logic will go here
40-
}}
42+
onPress={onForward}
4143
/>
4244
<IconButton
4345
icon={Icons.share}
4446
color={theme.colors.textPrimary}
4547
size={26}
4648
accessibilityLabel="Share"
47-
onPress={() => {
48-
// More actions (settings, share, etc.)
49-
}}
49+
onPress={onShare}
5050
/>
5151
<IconButton
5252
icon={

src/features/browser/screens/BrowserScreen/WebViewContainer.tsx

Lines changed: 100 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useScripts } from "@/src/features/scripts/hooks/useScripts";
2-
import { useCallback, useRef } from "react";
2+
import { forwardRef, useCallback, useRef } from "react";
33
import { StyleSheet, View, ViewProps } from "react-native";
44
import {
55
WebView,
@@ -24,95 +24,120 @@ function getInjectionScripts(
2424
type WebViewContainerProps = {
2525
bottomPadding?: number;
2626
onScrollDirectionChange?: (direction: "up" | "down") => void;
27+
goBack: () => void;
28+
goForward: () => void;
29+
reload: () => void;
30+
stopLoading: () => void;
2731
} & ViewProps;
2832

29-
export default function WebViewContainer({
30-
bottomPadding = 0,
31-
style,
32-
onScrollDirectionChange,
33-
...props
34-
}: WebViewContainerProps) {
35-
const { activeTab, updateTabById } = useBrowser();
36-
const { getScriptsByRunAt, logExecution } = useScripts();
37-
const webviewRef = useRef<WebView>(null);
33+
const WebViewContainer = forwardRef<WebView, WebViewContainerProps>(
34+
function WebViewContainer(
35+
{
36+
bottomPadding = 0,
37+
style,
38+
onScrollDirectionChange,
39+
goBack,
40+
goForward,
41+
reload,
42+
stopLoading,
43+
...props
44+
},
45+
ref
46+
) {
47+
const { activeTab, updateTabById } = useBrowser();
48+
const { getScriptsByRunAt, logExecution } = useScripts();
3849

39-
// Prepare URL
40-
const url = activeTab?.url ? normalizeUrl(activeTab.url) : "about:blank";
50+
// Prepare URL
51+
const url = activeTab?.url ? normalizeUrl(activeTab.url) : "about:blank";
4152

42-
// Inject scripts at different lifecycle points
43-
const injectedJavaScriptBeforeContentLoaded = activeTab?.url
44-
? getInjectionScripts(url, "document-start", getScriptsByRunAt)
45-
: "";
46-
const injectedJavaScript = activeTab?.url
47-
? getInjectionScripts(url, "document-ready", getScriptsByRunAt)
48-
: "";
53+
// Inject scripts at different lifecycle points
54+
const injectedJavaScriptBeforeContentLoaded = activeTab?.url
55+
? getInjectionScripts(url, "document-start", getScriptsByRunAt)
56+
: "";
57+
const injectedJavaScript = activeTab?.url
58+
? getInjectionScripts(url, "document-ready", getScriptsByRunAt)
59+
: "";
4960

50-
// Handle navigation events
51-
const onNavigationStateChange = useCallback(
52-
(navState: WebViewNavigation) => {
53-
if (!activeTab) return;
54-
updateTabById(activeTab.id, {
55-
url: navState.url,
56-
title: navState.title ?? "",
57-
canGoBack: navState.canGoBack,
58-
canGoForward: navState.canGoForward,
59-
isLoading: navState.loading,
60-
});
61-
},
62-
[activeTab, updateTabById]
63-
);
61+
// document-end: injected after page load
62+
const documentEndScripts = activeTab?.url
63+
? getInjectionScripts(url, "document-end", getScriptsByRunAt)
64+
: "";
65+
66+
// Handle navigation events
67+
const onNavigationStateChange = useCallback(
68+
(navState: WebViewNavigation) => {
69+
if (!activeTab) return;
70+
updateTabById(activeTab.id, {
71+
url: navState.url,
72+
title: navState.title ?? "",
73+
canGoBack: navState.canGoBack,
74+
canGoForward: navState.canGoForward,
75+
isLoading: navState.loading,
76+
});
77+
// Inject document-end scripts when loading finishes
78+
if (!navState.loading && documentEndScripts) {
79+
(ref as React.RefObject<WebView>)?.current?.injectJavaScript(
80+
documentEndScripts
81+
);
82+
}
83+
},
84+
[activeTab, updateTabById, documentEndScripts]
85+
);
6486

65-
// Handle JS execution results
66-
const onMessage = useCallback((event: WebViewMessageEvent) => {
67-
// TODO: Log execution results
68-
}, []);
87+
// Handle JS execution results
88+
const onMessage = useCallback((event: WebViewMessageEvent) => {
89+
// TODO: Log execution results
90+
}, []);
6991

70-
// Inject CSS to add bottom padding to the body
71-
const injectedCSS = `
92+
// Inject CSS to add bottom padding to the body
93+
const injectedCSS = `
7294
const style = document.createElement('style');
7395
style.innerHTML = 'body { padding-bottom: ${bottomPadding}px !important; box-sizing: border-box; }';
7496
document.head.appendChild(style);
7597
`;
7698

77-
const lastScrollY = useRef(0);
78-
const lastDirection = useRef<"up" | "down" | null>(null); // pixels
79-
const handleScroll = (event: any) => {
80-
const currentY = event.nativeEvent.contentOffset?.y ?? 0;
81-
const diff = currentY - lastScrollY.current;
99+
const lastScrollY = useRef(0);
100+
const lastDirection = useRef<"up" | "down" | null>(null); // pixels
101+
const handleScroll = (event: any) => {
102+
const currentY = event.nativeEvent.contentOffset?.y ?? 0;
103+
const diff = currentY - lastScrollY.current;
82104

83-
if (Math.abs(diff) > SCROLL_DIRECTION_THRESHOLD) {
84-
const newDirection = diff > 0 ? "down" : "up";
85-
if (newDirection !== lastDirection.current) {
86-
onScrollDirectionChange?.(newDirection);
87-
lastDirection.current = newDirection;
105+
if (Math.abs(diff) > SCROLL_DIRECTION_THRESHOLD) {
106+
const newDirection = diff > 0 ? "down" : "up";
107+
if (newDirection !== lastDirection.current) {
108+
onScrollDirectionChange?.(newDirection);
109+
lastDirection.current = newDirection;
110+
}
88111
}
89-
}
90-
lastScrollY.current = currentY;
91-
};
112+
lastScrollY.current = currentY;
113+
};
92114

93-
return (
94-
<View style={[styles.container, style]} {...props}>
95-
<WebView
96-
ref={webviewRef}
97-
source={{ uri: url }}
98-
injectedJavaScriptBeforeContentLoaded={
99-
injectedJavaScriptBeforeContentLoaded
100-
}
101-
injectedJavaScript={injectedJavaScript}
102-
onNavigationStateChange={onNavigationStateChange}
103-
onMessage={onMessage}
104-
startInLoadingState={true}
105-
allowsInlineMediaPlayback
106-
javaScriptEnabled
107-
domStorageEnabled
108-
setSupportMultipleWindows={false}
109-
allowsBackForwardNavigationGestures
110-
originWhitelist={["*"]}
111-
onScroll={handleScroll}
112-
/>
113-
</View>
114-
);
115-
}
115+
return (
116+
<View style={[styles.container, style]} {...props}>
117+
<WebView
118+
ref={ref}
119+
source={{ uri: url }}
120+
injectedJavaScriptBeforeContentLoaded={
121+
injectedJavaScriptBeforeContentLoaded
122+
}
123+
injectedJavaScript={injectedJavaScript}
124+
onNavigationStateChange={onNavigationStateChange}
125+
onMessage={onMessage}
126+
startInLoadingState={true}
127+
allowsInlineMediaPlayback
128+
javaScriptEnabled
129+
domStorageEnabled
130+
setSupportMultipleWindows={false}
131+
allowsBackForwardNavigationGestures
132+
originWhitelist={["*"]}
133+
onScroll={handleScroll}
134+
/>
135+
</View>
136+
);
137+
}
138+
);
139+
140+
export default WebViewContainer;
116141

117142
const styles = StyleSheet.create({
118143
container: {

src/features/browser/screens/BrowserScreen/index.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useTheme } from "@/src/theme/useTheme";
2-
import { useCallback, useEffect, useMemo, useState } from "react";
2+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
33
import { Animated, StyleSheet, View } from "react-native";
44
import {
55
KeyboardEvents,
@@ -82,6 +82,14 @@ export default function BrowserScreen() {
8282
[height, offset, bottomContainerY]
8383
);
8484

85+
// Navigation handlers for WebView
86+
const webviewRef = useRef<any>(null);
87+
88+
const goBack = () => webviewRef.current?.goBack?.();
89+
const goForward = () => webviewRef.current?.goForward?.();
90+
const reload = () => webviewRef.current?.reload?.();
91+
const stopLoading = () => webviewRef.current?.stopLoading?.();
92+
8593
return (
8694
<View style={{ flex: 1 }}>
8795
<SafeAreaView
@@ -93,6 +101,11 @@ export default function BrowserScreen() {
93101
>
94102
<View style={styles.webViewContainer}>
95103
<WebViewContainer
104+
ref={webviewRef}
105+
goBack={goBack}
106+
goForward={goForward}
107+
reload={reload}
108+
stopLoading={stopLoading}
96109
bottomPadding={isKeyboardVisible ? 16 : 0}
97110
onScrollDirectionChange={setScrollDirection}
98111
/>
@@ -101,11 +114,15 @@ export default function BrowserScreen() {
101114

102115
<Animated.View style={[styles.bottomContainer, bottomContainerStyle]}>
103116
{showAddressBar && (
104-
<AddressBar onFocusChange={onAddressBarFocusChange} />
117+
<AddressBar
118+
onReload={reload}
119+
onStop={stopLoading}
120+
onFocusChange={onAddressBarFocusChange}
121+
/>
105122
)}
106123
{!showAddressBar && <AddressBarDisplay />}
107124
{/* Toolbar contains the SafeAreaView */}
108-
<Toolbar />
125+
<Toolbar onBack={goBack} onForward={goForward} />
109126
</Animated.View>
110127
</View>
111128
);

0 commit comments

Comments
 (0)