Improve loading UX (#541)

This commit is contained in:
Leendert de Borst
2025-02-11 14:52:25 +01:00
parent a3d8242dc4
commit bd833414ad
4 changed files with 83 additions and 53 deletions

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import { useAuth } from './context/AuthContext';
import { useDb } from './context/DbContext';
import { useMinDurationLoading } from './hooks/useMinDurationLoading';
import Header from './components/Layout/Header';
import BottomNav from './components/Layout/BottomNav';
@@ -15,6 +14,7 @@ import CredentialDetails from './pages/CredentialDetails';
import EmailDetails from './pages/EmailDetails';
import Settings from './pages/Settings';
import GlobalStateChangeHandler from './components/GlobalStateChangeHandler';
import { useLoading } from './context/LoadingContext';
/**
* Route configuration.
@@ -31,9 +31,9 @@ type RouteConfig = {
*/
const App: React.FC = () => {
const authContext = useAuth();
const dbContext = useDb();
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const { isInitialLoading } = useLoading();
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [message, setMessage] = useState<string | null>(null);
// Add these route configurations
@@ -47,14 +47,11 @@ const App: React.FC = () => {
{ path: '/settings', element: <Settings />, showBackButton: true, title: 'Settings' },
];
/**
* Set loading state to false when auth and db are initialized.
*/
useEffect(() => {
if (authContext.isInitialized && dbContext.dbInitialized) {
if (!isInitialLoading) {
setIsLoading(false);
}
}, [authContext.isInitialized, dbContext.dbInitialized, setIsLoading]);
}, [isInitialLoading, setIsLoading]);
/**
* Print global message if it exists.
@@ -66,21 +63,15 @@ const App: React.FC = () => {
}
}, [authContext, authContext.globalMessage]);
if (isLoading) {
return (
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col">
<div className="flex-1 overflow-y-auto" style={{ paddingTop: '64px' }}>
<div className="p-4 mt-20 dark:bg-gray-900 h-full flex items-center justify-center">
<LoadingSpinner />
</div>
</div>
</div>
);
}
return (
<Router>
<div className="min-h-screen min-w-[350px] bg-white dark:bg-gray-900 flex flex-col">
{isLoading && (
<div className="fixed inset-0 bg-white dark:bg-gray-900 z-50 flex items-center justify-center">
<LoadingSpinner />
</div>
)}
<GlobalStateChangeHandler />
<Header
toggleUserMenu={() => setIsUserMenuOpen(!isUserMenuOpen)}

View File

@@ -5,6 +5,8 @@ type LoadingContextType = {
isLoading: boolean;
showLoading: () => void;
hideLoading: () => void;
isInitialLoading: boolean;
setIsInitialLoading: (isInitialLoading: boolean) => void;
}
/**
@@ -16,6 +18,16 @@ const LoadingContext = createContext<LoadingContextType | undefined>(undefined);
* Loading provider
*/
export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
/**
* Initial loading state for when extension is first loaded. This initial loading state is
* hidden by the component that is rendered when the extension is first loaded to prevent
* multiple loading spinners from being shown.
*/
const [isInitialLoading, setIsInitialLoading] = useState(true);
/**
* Loading state that can be used by other components during normal operation.
*/
const [isLoading, setIsLoading] = useState(false);
/**
@@ -29,7 +41,7 @@ export const LoadingProvider: React.FC<{ children: React.ReactNode }> = ({ child
const hideLoading = (): void => setIsLoading(false);
return (
<LoadingContext.Provider value={{ isLoading, showLoading, hideLoading }}>
<LoadingContext.Provider value={{ isLoading, showLoading, hideLoading, isInitialLoading, setIsInitialLoading }}>
<LoadingSpinnerFullScreen />
{children}
</LoadingContext.Provider>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { useDb } from '../context/DbContext';
import { Credential } from '../../shared/types/Credential';
import { Buffer } from 'buffer';
@@ -9,6 +9,8 @@ import { VaultResponse } from '../../shared/types/webapi/VaultResponse';
import ReloadButton from '../components/ReloadButton';
import { useAuth } from '../context/AuthContext';
import { StatusResponse } from '../../shared/types/webapi/StatusResponse';
import LoadingSpinner from '../components/LoadingSpinner';
import { useMinDurationLoading } from '../hooks/useMinDurationLoading';
/**
* Credentials list page.
@@ -18,42 +20,18 @@ const CredentialsList: React.FC = () => {
const webApi = useWebApi();
const [credentials, setCredentials] = useState<Credential[]>([]);
const navigate = useNavigate();
const { showLoading, hideLoading } = useLoading();
const { showLoading, hideLoading, setIsInitialLoading } = useLoading();
const authContext = useAuth();
useEffect(() => {
/**
* Check if the extension is (still) supported by the API and if the local vault is up to date.
*/
const checkStatus = async (): Promise<void> => {
if (!dbContext?.sqliteClient) return;
const statusResponse = await webApi.get('Auth/status') as StatusResponse;
if (!statusResponse.supported) {
authContext.logout('This version of the AliasVault browser extension is outdated. Please update to the latest version.');
return;
}
if (statusResponse.vaultRevision > dbContext.vaultRevision) {
await onRefresh();
}
// Load credentials
try {
const results = dbContext.sqliteClient.getAllCredentials();
setCredentials(results);
} catch (err) {
console.error('Error loading credentials:', err);
}
};
checkStatus();
});
/**
* Loading state with minimum duration for more fluid UX.
*/
const [isLoading, setIsLoading] = useMinDurationLoading(true, 150);
/**
* Retrieve latest vault and refresh the page.
*/
const onRefresh = async () : Promise<void> => {
const onRefresh = useCallback(async () : Promise<void> => {
if (!dbContext?.sqliteClient) return;
try {
@@ -77,20 +55,64 @@ const CredentialsList: React.FC = () => {
try {
const results = dbContext.sqliteClient.getAllCredentials();
setCredentials(results);
setIsLoading(false);
setIsInitialLoading(false);
} catch (err) {
console.error('Error loading credentials:', err);
}
} catch (err) {
console.error('Refresh error:', err);
}
};
}, [dbContext, webApi, authContext, hideLoading, setIsInitialLoading, setIsLoading]);
useEffect(() => {
/**
* Check if the extension is (still) supported by the API and if the local vault is up to date.
*/
const checkStatus = async (): Promise<void> => {
if (!dbContext?.sqliteClient) return;
const statusResponse = await webApi.get('Auth/status') as StatusResponse;
if (!statusResponse.supported) {
authContext.logout('This version of the AliasVault browser extension is outdated. Please update to the latest version.');
return;
}
if (statusResponse.vaultRevision > dbContext.vaultRevision) {
await onRefresh();
}
// Load credentials
try {
const results = dbContext.sqliteClient.getAllCredentials();
setCredentials(results);
setIsLoading(false);
setIsInitialLoading(false);
} catch (err) {
console.error('Error loading credentials:', err);
}
};
checkStatus();
}, [authContext, dbContext?.sqliteClient, dbContext?.vaultRevision, onRefresh, webApi, setIsInitialLoading, setIsLoading]);
/**
* Manually refresh the credentials list.
*/
const onManualRefresh = async (): Promise<void> => {
showLoading();
await onRefresh();
hideLoading();
};
if (isLoading) {
return (
<div className="flex justify-center items-center p-8">
<LoadingSpinner />
</div>
);
}
return (
<div>
<div className="flex justify-between items-center">

View File

@@ -5,6 +5,7 @@ import Login from './Login';
import UnlockSuccess from './UnlockSuccess';
import { useNavigate } from 'react-router-dom';
import { useDb } from '../context/DbContext';
import { useLoading } from '../context/LoadingContext';
/**
* Home page that shows the correct page based on the user's authentication state.
@@ -14,6 +15,7 @@ const Home: React.FC = () => {
const authContext = useAuth();
const dbContext = useDb();
const navigate = useNavigate();
const { setIsInitialLoading } = useLoading();
const [isInlineUnlockMode, setIsInlineUnlockMode] = useState(false);
const needsUnlock = (!authContext.isLoggedIn && authContext.isInitialized) || (!dbContext.dbAvailable && dbContext.dbInitialized);
@@ -28,6 +30,9 @@ const Home: React.FC = () => {
}
}, [isLoggedIn, needsUnlock, isInlineUnlockMode, navigate]);
// Set initial loading state to false once the page is loaded until here.
setIsInitialLoading(false);
if (!isLoggedIn) {
return <Login />;
}