mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 07:07:59 -04:00
Remember last visited page in browser extension and navigate back on reopen (#928)
This commit is contained in:
committed by
Leendert de Borst
parent
0eebaddf04
commit
7776fb6d82
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user