mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-19 23:28:23 -04:00
Improve loading UX (#541)
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user