Remember last visited page in browser extension and navigate back on reopen (#928)

This commit is contained in:
Leendert de Borst
2025-06-17 19:19:23 +02:00
committed by Leendert de Borst
parent 0eebaddf04
commit 7776fb6d82
7 changed files with 145 additions and 45 deletions

View File

@@ -19,7 +19,9 @@ import Logout from '@/entrypoints/popup/pages/Logout';
import Settings from '@/entrypoints/popup/pages/Settings';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
import '@/entrypoints/popup/style.css';
import { NavigationProvider } from './context/NavigationContext';
/**
* Route configuration.
@@ -74,44 +76,45 @@ const App: React.FC = () => {
return (
<Router>
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
{isLoading && (
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
<NavigationProvider>
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col max-h-[600px]">
{isLoading && (
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
<GlobalStateChangeHandler />
<Header
routes={routes}
rightButtons={headerButtons}
/>
<GlobalStateChangeHandler />
<Header
routes={routes}
rightButtons={headerButtons}
/>
<main
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
style={{
paddingTop: '64px',
height: 'calc(100% - 120px)',
}}
>
<div className="p-4 mb-16">
{message && (
<p className="text-red-500 mb-4">{message}</p>
)}
<Routes>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
element={route.element}
/>
))}
</Routes>
</div>
</main>
<BottomNav />
</div>
<main
className="flex-1 overflow-y-auto bg-gray-100 dark:bg-gray-900"
style={{
paddingTop: '64px',
height: 'calc(100% - 120px)',
}}
>
<div className="p-4 mb-16">
{message && (
<p className="text-red-500 mb-4">{message}</p>
)}
<Routes>
{routes.map((route) => (
<Route
key={route.path}
path={route.path}
element={route.element}
/>
))}
</Routes>
</div>
</main>
<BottomNav />
</div>
</NavigationProvider>
</Router>
);
};

View File

@@ -18,9 +18,13 @@ const BottomNav: React.FC = () => {
// Add effect to update currentTab based on route
useEffect(() => {
const path = location.pathname.substring(1) as TabName;
if (['credentials', 'emails', 'settings'].includes(path)) {
setCurrentTab(path);
const path = location.pathname.substring(1); // Remove leading slash
const tabNames: TabName[] = ['credentials', 'emails', 'settings'];
// Find the first tab name that matches the start of the path
const matchingTab = tabNames.find(tab => path === tab || path.startsWith(`${tab}/`));
if (matchingTab) {
setCurrentTab(matchingTab);
}
}, [location]);

View File

@@ -0,0 +1,85 @@
import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { storage } from '#imports';
const LAST_VISITED_PAGE_KEY = 'local:lastVisitedPage';
const LAST_VISITED_TIME_KEY = 'local:lastVisitedTime';
const PAGE_MEMORY_DURATION = 120 * 1000; // 2 minutes in milliseconds
type NavigationContextType = {
storeCurrentPage: () => Promise<void>;
restoreLastPage: () => Promise<void>;
};
const NavigationContext = createContext<NavigationContextType | undefined>(undefined);
/**
* Navigation provider component that handles storing and restoring the last visited page.
*/
export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const location = useLocation();
const navigate = useNavigate();
const [isInitialized, setIsInitialized] = useState(false);
/**
* Store the current page path and timestamp in storage.
*/
const storeCurrentPage = useCallback(async (): Promise<void> => {
await storage.setItem(LAST_VISITED_PAGE_KEY, location.pathname);
await storage.setItem(LAST_VISITED_TIME_KEY, Date.now());
}, [location.pathname]);
/**
* Restore the last visited page if it was visited within the memory duration.
*/
const restoreLastPage = useCallback(async (): Promise<void> => {
const lastPage = await storage.getItem(LAST_VISITED_PAGE_KEY) as string;
const lastVisitTime = await storage.getItem(LAST_VISITED_TIME_KEY) as number;
if (lastPage && lastVisitTime) {
const timeSinceLastVisit = Date.now() - lastVisitTime;
if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
navigate(lastPage);
}
}
}, [navigate]);
// Store the current page whenever it changes
useEffect(() => {
if (isInitialized) {
storeCurrentPage();
}
}, [location.pathname, isInitialized, storeCurrentPage]);
// Restore the last page on initial load
useEffect(() => {
if (!isInitialized) {
restoreLastPage();
setIsInitialized(true);
}
}, [isInitialized, restoreLastPage]);
const contextValue = useMemo(() => ({
storeCurrentPage,
restoreLastPage
}), [storeCurrentPage, restoreLastPage]);
return (
<NavigationContext.Provider value={contextValue}>
{children}
</NavigationContext.Provider>
);
};
/**
* Hook to access the navigation context.
* @returns The navigation context
*/
export const useNavigation = (): NavigationContextType => {
const context = useContext(NavigationContext);
if (context === undefined) {
throw new Error('useNavigation must be used within a NavigationProvider');
}
return context;
};

View File

@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react';
import * as Yup from 'yup';
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
import { AppInfo } from '@/utils/AppInfo';
import { GLOBAL_AUTOFILL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/utils/Constants';
@@ -55,6 +57,7 @@ const AuthSettings: React.FC = () => {
const [customClientUrl, setCustomClientUrl] = useState<string>('');
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
const [errors, setErrors] = useState<{ apiUrl?: string; clientUrl?: string }>({});
const { setIsInitialLoading } = useLoading();
useEffect(() => {
/**
@@ -83,10 +86,11 @@ const AuthSettings: React.FC = () => {
} else {
setSelectedOption(DEFAULT_OPTIONS[0].value);
}
setIsInitialLoading(false);
};
loadStoredSettings();
}, []);
}, [setIsInitialLoading]);
/**
* Handle option change

View File

@@ -106,7 +106,7 @@ const CredentialAddEdit: React.FC = () => {
setTimeout(() => {
serviceNameRef.current?.focus();
}, 100);
setIsInitialLoading(false);
return;
}
@@ -122,6 +122,7 @@ const CredentialAddEdit: React.FC = () => {
});
setMode('manual');
setIsInitialLoading(false);
// On create mode, focus the service name field after a short delay to ensure the component is mounted
} else {
@@ -130,6 +131,7 @@ const CredentialAddEdit: React.FC = () => {
}
} catch (err) {
console.error('Error loading credential:', err);
setIsInitialLoading(false);
}
}, [dbContext.sqliteClient, id, navigate, setIsInitialLoading, setValue]);

View File

@@ -29,7 +29,7 @@ const Login: React.FC = () => {
username: '',
password: '',
});
const { showLoading, hideLoading } = useLoading();
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
const [rememberMe, setRememberMe] = useState(true);
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
const [passwordHashString, setPasswordHashString] = useState<string | null>(null);
@@ -53,9 +53,10 @@ const Login: React.FC = () => {
}
setClientUrl(clientUrl);
setIsInitialLoading(false);
};
loadClientUrl();
}, []);
}, [setIsInitialLoading]);
/**
* Handle submit

View File

@@ -29,7 +29,7 @@ const Unlock: React.FC = () => {
const [password, setPassword] = useState('');
const [error, setError] = useState<string | null>(null);
const { showLoading, hideLoading } = useLoading();
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
useEffect(() => {
/**
@@ -41,10 +41,11 @@ const Unlock: React.FC = () => {
if (statusError !== null) {
await webApi.logout(statusError);
}
setIsInitialLoading(false);
};
checkStatus();
}, [webApi, authContext]);
}, [webApi, authContext, setIsInitialLoading]);
/**
* Handle submit