All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
- Implemented Button component with various props for customization. - Created Modal component with header, content, and footer subcomponents. - Added Spinner component for loading indicators. - Developed Toast component for displaying notifications. - Introduced Tooltip component for contextual hints with keyboard shortcuts. - Added corresponding CSS modules for styling each component. - Updated index file to export new components. - Configured TypeScript settings for the UI package.
182 lines
5.6 KiB
TypeScript
182 lines
5.6 KiB
TypeScript
import { useEffect, useState, type ReactNode } from 'react';
|
|
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
|
import { IconContext } from '@phosphor-icons/react';
|
|
import { LoginPage } from './components/auth/LoginPage';
|
|
import { RegisterPage } from './components/auth/RegisterPage';
|
|
import { InviteAcceptPage } from './components/auth/InviteAcceptPage';
|
|
import { AppLayout } from './components/layout/AppLayout';
|
|
import { MobileYouPage } from './components/layout/MobileYouPage';
|
|
import { TitleBar } from './components/layout/TitleBar';
|
|
import { ChannelView } from './components/channel/ChannelView';
|
|
import Recovery from './pages/Recovery';
|
|
import { usePlatform } from './platform';
|
|
import { useSearch } from './contexts/SearchContext';
|
|
import { useSystemBars } from './hooks/useSystemBars';
|
|
import './global.css';
|
|
|
|
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
|
|
type AuthState = 'loading' | 'authenticated' | 'unauthenticated';
|
|
|
|
function AuthGuard({ children }: { children: ReactNode }) {
|
|
const [authState, setAuthState] = useState<AuthState>('loading');
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const { session, settings } = usePlatform();
|
|
const searchCtx = useSearch();
|
|
useSystemBars(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
async function restoreSession() {
|
|
if (sessionStorage.getItem('privateKey') && sessionStorage.getItem('signingKey')) {
|
|
searchCtx?.initialize();
|
|
if (!cancelled) setAuthState('authenticated');
|
|
return;
|
|
}
|
|
|
|
if (session) {
|
|
try {
|
|
const savedSession = await session.load();
|
|
if (savedSession && savedSession.savedAt && Date.now() - savedSession.savedAt < THIRTY_DAYS_MS) {
|
|
localStorage.setItem('userId', savedSession.userId);
|
|
localStorage.setItem('username', savedSession.username);
|
|
if (savedSession.publicKey) localStorage.setItem('publicKey', savedSession.publicKey);
|
|
sessionStorage.setItem('signingKey', savedSession.signingKey);
|
|
sessionStorage.setItem('privateKey', savedSession.privateKey);
|
|
if (savedSession.masterKey) sessionStorage.setItem('masterKey', savedSession.masterKey);
|
|
if (savedSession.searchDbKey) sessionStorage.setItem('searchDbKey', savedSession.searchDbKey);
|
|
searchCtx?.initialize();
|
|
if (settings) {
|
|
try {
|
|
const savedPrefs = await settings.get(`userPrefs_${savedSession.userId}`);
|
|
if (savedPrefs && typeof savedPrefs === 'object') {
|
|
localStorage.setItem(`userPrefs_${savedSession.userId}`, JSON.stringify(savedPrefs));
|
|
}
|
|
} catch {}
|
|
}
|
|
if (!cancelled) setAuthState('authenticated');
|
|
return;
|
|
}
|
|
if (savedSession?.savedAt) {
|
|
await session.clear();
|
|
}
|
|
} catch (err) {
|
|
console.error('Session restore failed:', err);
|
|
}
|
|
}
|
|
|
|
if (!cancelled) setAuthState('unauthenticated');
|
|
}
|
|
|
|
restoreSession();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (authState === 'loading') return;
|
|
|
|
const isAuthPage =
|
|
location.pathname === '/login' ||
|
|
location.pathname === '/register' ||
|
|
location.pathname === '/recovery' ||
|
|
location.pathname.startsWith('/invite/');
|
|
const hasSession = !!(sessionStorage.getItem('privateKey') && sessionStorage.getItem('signingKey'));
|
|
|
|
// Invite accept runs regardless of session — if the viewer is
|
|
// already logged in, the InviteAcceptPage itself will forward
|
|
// them home after stashing the keys. Don't redirect them away.
|
|
const isInvitePage = location.pathname.startsWith('/invite/');
|
|
|
|
if (hasSession && isAuthPage && !isInvitePage) {
|
|
navigate('/channels/@me', { replace: true });
|
|
} else if (!hasSession && !isAuthPage) {
|
|
navigate('/login', { replace: true });
|
|
}
|
|
}, [authState, location.pathname]);
|
|
|
|
if (authState === 'loading') {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
height: '100vh',
|
|
backgroundColor: 'var(--background-primary)',
|
|
color: 'var(--text-primary)',
|
|
fontSize: 16,
|
|
}}
|
|
>
|
|
Loading...
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <>{children}</>;
|
|
}
|
|
|
|
function DMHomePage() {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flex: 1,
|
|
color: 'var(--text-secondary)',
|
|
}}
|
|
>
|
|
<div style={{ textAlign: 'center' }}>
|
|
<h2 style={{ color: 'var(--text-primary)', marginBottom: 8 }}>Welcome to Brycord</h2>
|
|
<p>Select a conversation or start a new one</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ServerPage() {
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
flex: 1,
|
|
color: 'var(--text-secondary)',
|
|
}}
|
|
>
|
|
<p>Select a channel to start chatting</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
return (
|
|
<IconContext.Provider value={{ color: 'currentColor', weight: 'fill' }}>
|
|
<TitleBar />
|
|
<AuthGuard>
|
|
<Routes>
|
|
<Route path="/login" element={<LoginPage />} />
|
|
<Route path="/register" element={<RegisterPage />} />
|
|
<Route path="/invite/:code" element={<InviteAcceptPage />} />
|
|
<Route path="/recovery" element={<Recovery />} />
|
|
<Route path="/channels/*" element={<AppLayout />}>
|
|
<Route path="@me" element={<DMHomePage />} />
|
|
<Route path="@me/:channelId" element={<ChannelView />} />
|
|
<Route path="you" element={<MobileYouPage />} />
|
|
<Route path=":serverId" element={<ServerPage />} />
|
|
<Route path=":serverId/:channelId" element={<ChannelView />} />
|
|
</Route>
|
|
<Route path="*" element={<Navigate to="/channels/@me" replace />} />
|
|
</Routes>
|
|
</AuthGuard>
|
|
</IconContext.Provider>
|
|
);
|
|
}
|
|
|
|
export default App;
|