Some checks failed
Backend Tests / Static Checks (push) Has been cancelled
Backend Tests / Tests (other) (push) Has been cancelled
Backend Tests / Tests (plugin) (push) Has been cancelled
Backend Tests / Tests (server) (push) Has been cancelled
Backend Tests / Tests (store) (push) Has been cancelled
Build Canary Image / build-frontend (push) Has been cancelled
Build Canary Image / build-push (linux/amd64) (push) Has been cancelled
Build Canary Image / build-push (linux/arm64) (push) Has been cancelled
Build Canary Image / merge (push) Has been cancelled
Frontend Tests / Lint (push) Has been cancelled
Frontend Tests / Build (push) Has been cancelled
Proto Linter / Lint Protos (push) Has been cancelled
658 lines
18 KiB
Plaintext
658 lines
18 KiB
Plaintext
# Memos Frontend Documentation
|
|
|
|
## Authentication and Middleware System
|
|
|
|
### Frontend Authentication Architecture
|
|
|
|
The frontend authentication system uses a combination of:
|
|
|
|
1. **React Context** for state management
|
|
2. **Route-based guards** for access control
|
|
3. **Connect RPC interceptors** for API authentication
|
|
4. **Layout-based protection** for UI components
|
|
|
|
### Core Authentication Components
|
|
|
|
#### 1. Auth Context (`web/src/contexts/AuthContext.tsx`)
|
|
|
|
```typescript
|
|
// Manages global authentication state
|
|
const AuthContext = createContext<AuthContextType>({
|
|
currentUser: undefined,
|
|
userGeneralSetting: undefined,
|
|
isInitialized: false,
|
|
isLoading: true,
|
|
initialize: async () => {},
|
|
logout: () => {},
|
|
refetchSettings: async () => {}
|
|
});
|
|
|
|
// Provides authentication state to entire app
|
|
function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
// Handles token initialization, user fetching, logout logic
|
|
}
|
|
```
|
|
|
|
#### 2. Route Protection System
|
|
|
|
**Public Routes** (no auth required):
|
|
|
|
- `/auth` - Authentication pages
|
|
- `/auth/signup` - User registration
|
|
- `/auth/callback` - OAuth callback
|
|
- `/explore` - Public explore page
|
|
- `/u/:username` - User profiles
|
|
- `/memos/:uid` - Individual memo details
|
|
|
|
**Private Routes** (auth required):
|
|
|
|
- `/` (root) - Main dashboard
|
|
- `/attachments` - File attachments
|
|
- `/inbox` - Notifications
|
|
- `/archived` - Archived memos
|
|
- `/setting` - User settings
|
|
|
|
#### 3. Layout-Based Authentication Guards
|
|
|
|
**RootLayout** (`web/src/layouts/RootLayout.tsx`):
|
|
|
|
```typescript
|
|
const RootLayout = () => {
|
|
const currentUser = useCurrentUser();
|
|
|
|
useEffect(() => {
|
|
if (!currentUser) {
|
|
redirectOnAuthFailure(); // Redirects to /auth
|
|
}
|
|
}, [currentUser]);
|
|
|
|
// Renders navigation and main content
|
|
return (
|
|
<div className="w-full min-h-full flex flex-row">
|
|
<Navigation />
|
|
<main>
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
**Route Configuration** (`web/src/router/index.tsx`):
|
|
|
|
```typescript
|
|
const router = createBrowserRouter([
|
|
{
|
|
path: "/",
|
|
element: <App />, // Handles instance initialization
|
|
children: [
|
|
{
|
|
path: "/auth", // Public routes
|
|
children: [
|
|
{ path: "", element: <SignIn /> },
|
|
{ path: "signup", element: <SignUp /> },
|
|
{ path: "callback", element: <AuthCallback /> }
|
|
]
|
|
},
|
|
{
|
|
path: "/", // Protected routes (wrapped in RootLayout)
|
|
element: <RootLayout />, // Auth guard here
|
|
children: [
|
|
{
|
|
element: <MainLayout />, // Main app layout
|
|
children: [
|
|
{ path: "", element: <Home /> },
|
|
{ path: "explore", element: <Explore /> }
|
|
// ... other protected routes
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]);
|
|
```
|
|
|
|
### API Authentication Interceptors
|
|
|
|
#### Connect RPC Interceptor (`web/src/connect.ts`):
|
|
|
|
```typescript
|
|
const authInterceptor: Interceptor = (next) => async (req) => {
|
|
const isRetryAttempt = req.header.get(RETRY_HEADER) === RETRY_HEADER_VALUE;
|
|
const token = await getRequestToken();
|
|
setAuthorizationHeader(req, token);
|
|
|
|
try {
|
|
return await next(req);
|
|
} catch (error) {
|
|
// Handle 401 Unauthorized
|
|
if (error.code === Code.Unauthenticated && !isRetryAttempt) {
|
|
try {
|
|
// Attempt token refresh
|
|
const newToken = await refreshAndGetAccessToken();
|
|
setAuthorizationHeader(req, newToken);
|
|
req.header.set(RETRY_HEADER, RETRY_HEADER_VALUE);
|
|
return await next(req);
|
|
} catch (refreshError) {
|
|
redirectOnAuthFailure(); // Redirect to login
|
|
throw refreshError;
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
```
|
|
|
|
#### Token Management
|
|
|
|
- **Storage**: Access tokens stored in memory (not localStorage)
|
|
- **Refresh**: Automatic token refresh on 401 errors
|
|
- **Expiration**: Proactive refresh on tab focus
|
|
- **Cleanup**: Tokens cleared on logout/auth failure
|
|
|
|
### Authentication Flow
|
|
|
|
#### 1. App Initialization (`web/src/main.tsx`)
|
|
|
|
```typescript
|
|
// Early theme/locale setup to prevent flash
|
|
applyThemeEarly();
|
|
applyLocaleEarly();
|
|
|
|
// Initialize auth and instance contexts
|
|
<AuthProvider>
|
|
<InstanceProvider>
|
|
<RouterProvider router={router} />
|
|
</InstanceProvider>
|
|
</AuthProvider>
|
|
```
|
|
|
|
#### 2. Authentication Check (`web/src/utils/auth-redirect.ts`)
|
|
|
|
```typescript
|
|
export function redirectOnAuthFailure(): void {
|
|
const currentPath = window.location.pathname;
|
|
|
|
// Allow public routes
|
|
if (isPublicRoute(currentPath)) return;
|
|
|
|
// Redirect private routes to auth
|
|
if (isPrivateRoute(currentPath)) {
|
|
clearAccessToken();
|
|
window.location.replace(ROUTES.AUTH);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### 3. User Session Management
|
|
|
|
- Token refresh on window focus
|
|
- Automatic logout on token expiration
|
|
- Context cleanup on logout
|
|
- Query cache invalidation
|
|
|
|
### Key Security Features
|
|
|
|
- **Token Storage**: In-memory only (security best practice)
|
|
- **Automatic Refresh**: Handles token rotation seamlessly
|
|
- **Route Guards**: Prevent unauthorized access to protected routes
|
|
- **Context Isolation**: Auth state managed centrally
|
|
- **Error Handling**: Graceful degradation on auth failures
|
|
- **Session Cleanup**: Complete state reset on logout
|
|
|
|
### Related Files
|
|
|
|
- `web/src/contexts/AuthContext.tsx` - Authentication state management
|
|
- `web/src/layouts/RootLayout.tsx` - Main authentication guard
|
|
- `web/src/utils/auth-redirect.ts` - Route protection logic
|
|
- `web/src/connect.ts` - API authentication interceptors
|
|
- `web/src/router/index.tsx` - Route configuration
|
|
- `web/src/main.tsx` - App initialization
|
|
|
|
## Theme System Implementation
|
|
|
|
### Vite Build Process for Themes
|
|
|
|
The CSS themes are processed and built by **Vite** during the build process:
|
|
|
|
1. **Vite Configuration** (`web/vite.config.mts`)
|
|
- Uses `@tailwindcss/vite` plugin for CSS processing
|
|
- Tailwind CSS v4 handles theme token compilation
|
|
- Themes are bundled during `pnpm build` process
|
|
|
|
2. **CSS Processing Pipeline**
|
|
- Base styles imported in `web/src/index.css`
|
|
- Theme-specific CSS files located in `web/src/themes/`
|
|
- Vite processes `@theme inline` directives at build time
|
|
- Dynamic theme switching handled via JavaScript at runtime
|
|
|
|
### Theme Architecture
|
|
|
|
#### Core Theme Files
|
|
|
|
- `web/src/index.css` - Main CSS entry point
|
|
- `web/src/themes/default.css` - Base theme with Tailwind token mappings
|
|
- `web/src/themes/default-dark.css` - Dark theme variables
|
|
- `web/src/themes/paper.css` - Paper-style theme
|
|
- `web/src/utils/theme.ts` - Theme loading and management logic
|
|
|
|
#### How Themes Are Built
|
|
|
|
##### 1. Build Time Processing (Vite + Tailwind)
|
|
|
|
```css
|
|
/* In web/src/themes/default.css */
|
|
@theme inline {
|
|
--color-background: var(--background);
|
|
--color-foreground: var(--foreground);
|
|
/* ... other CSS variables */
|
|
}
|
|
```
|
|
|
|
- Tailwind compiles these into static CSS classes
|
|
- Shared across all themes
|
|
- Optimized during Vite build process
|
|
|
|
##### 2. Runtime Theme Switching
|
|
|
|
- JavaScript dynamically injects theme CSS
|
|
- Uses `?raw` import to get CSS as string
|
|
- Injects `<style>` elements into document head
|
|
- Controlled by `web/src/utils/theme.ts`
|
|
|
|
### Theme Loading Mechanism
|
|
|
|
In `web/src/utils/theme.ts`:
|
|
|
|
```typescript
|
|
import defaultDarkThemeContent from "../themes/default-dark.css?raw";
|
|
import paperThemeContent from "../themes/paper.css?raw";
|
|
|
|
const THEME_CONTENT: Record<ResolvedTheme, string | null> = {
|
|
default: null, // Uses base CSS
|
|
"default-dark": defaultDarkThemeContent,
|
|
paper: paperThemeContent,
|
|
};
|
|
|
|
// Dynamically injects theme CSS
|
|
const injectThemeStyle = (theme: ResolvedTheme): void => {
|
|
if (theme === "default") return; // Use base CSS
|
|
|
|
const css = THEME_CONTENT[theme];
|
|
if (css) {
|
|
const style = document.createElement("style");
|
|
style.id = "instance-theme";
|
|
style.textContent = css;
|
|
document.head.appendChild(style);
|
|
}
|
|
};
|
|
```
|
|
|
|
### Available Themes
|
|
|
|
1. **System** (`system`) - Follows OS preference
|
|
2. **Light** (`default`) - Default light theme
|
|
3. **Dark** (`default-dark`) - Dark mode theme
|
|
4. **Paper** (`paper`) - Paper-style theme
|
|
|
|
### Theme Selection Components
|
|
|
|
- `web/src/components/ThemeSelect.tsx` - Theme dropdown selector
|
|
- `web/src/pages/UserSetting.tsx` - User profile theme settings
|
|
- `web/src/contexts/InstanceContext.tsx` - Instance-wide theme defaults
|
|
|
|
### Build Process Commands
|
|
|
|
```bash
|
|
# Development
|
|
pnpm dev
|
|
|
|
# Production build (processes themes)
|
|
pnpm build
|
|
|
|
# The build process:
|
|
# 1. Vite processes Tailwind CSS
|
|
# 2. Theme CSS files are bundled
|
|
# 3. Dynamic theme loading code is included
|
|
# 4. Output goes to dist/ directory
|
|
```
|
|
|
|
### Key Technical Details
|
|
|
|
- **CSS-in-JS Approach**: Themes are injected as `<style>` elements
|
|
- **Tree Shaking**: Unused theme CSS is removed during build
|
|
- **Hot Reloading**: Theme changes reflect instantly in development
|
|
- **Performance**: Theme switching is instant (no page reload)
|
|
- **Storage**: Theme preference stored in localStorage
|
|
- **Fallback**: Defaults to "system" theme if none selected
|
|
|
|
The theme system leverages Vite's build optimization while maintaining runtime flexibility for dynamic theme switching.
|
|
|
|
## Menu and Navigation System
|
|
|
|
### Navigation Component Architecture
|
|
|
|
The menu/navigation system is implemented through the `Navigation` component which serves as the main sidebar navigation.
|
|
|
|
### Core Navigation Component (`web/src/components/Navigation.tsx`)
|
|
|
|
```typescript
|
|
interface NavLinkItem {
|
|
id: string;
|
|
path: string;
|
|
title: string;
|
|
icon: React.ReactNode;
|
|
}
|
|
|
|
const Navigation = (props: { collapsed?: boolean; className?: string }) => {
|
|
const currentUser = useCurrentUser();
|
|
const { data: notifications = [] } = useNotifications();
|
|
|
|
// Navigation items are defined as objects
|
|
const homeNavLink: NavLinkItem = {
|
|
id: "header-memos",
|
|
path: Routes.ROOT,
|
|
title: t("common.memos"),
|
|
icon: <LibraryIcon className="w-6 h-auto shrink-0" />
|
|
};
|
|
|
|
// Conditional navigation based on authentication state
|
|
const navLinks: NavLinkItem[] = currentUser
|
|
? [homeNavLink, exploreNavLink, attachmentsNavLink, inboxNavLink]
|
|
: [exploreNavLink, signInNavLink];
|
|
}
|
|
```
|
|
|
|
### Menu Items Configuration
|
|
|
|
#### For Authenticated Users
|
|
|
|
1. **Home** (`/`) - Main memos dashboard
|
|
- Icon: `LibraryIcon`
|
|
- Shows user's memos
|
|
|
|
2. **Explore** (`/explore`) - Public memos exploration
|
|
- Icon: `EarthIcon`
|
|
- Browse public content
|
|
|
|
3. **Attachments** (`/attachments`) - File management
|
|
- Icon: `PaperclipIcon`
|
|
- Manage uploaded files
|
|
|
|
4. **Inbox** (`/inbox`) - Notifications
|
|
- Icon: `BellIcon` with badge
|
|
- Shows unread notification count
|
|
|
|
#### For Unauthenticated Users
|
|
|
|
1. **Explore** (`/explore`) - Public content browsing
|
|
2. **Sign In** (`/auth`) - Authentication page
|
|
- Icon: `UserCircleIcon`
|
|
|
|
### Navigation Layout Structure
|
|
|
|
#### Desktop (>768px)
|
|
|
|
- Fixed sidebar on left (`w-16` when collapsed, wider when expanded)
|
|
- `RootLayout` renders `Navigation` component vertically
|
|
- Collapsed state shows icons only with tooltips
|
|
- Expanded state shows icons + text labels
|
|
|
|
#### Mobile (<768px)
|
|
|
|
- `NavigationDrawer` component in mobile header
|
|
- Slide-out drawer from left side
|
|
- Full-width navigation when opened
|
|
|
|
### Navigation Implementation
|
|
|
|
```typescript
|
|
// In RootLayout.tsx
|
|
{sm && (
|
|
<div className="fixed top-0 left-0 h-full w-16">
|
|
<Navigation className="py-4" collapsed={true} />
|
|
</div>
|
|
)}
|
|
|
|
// In MobileHeader.tsx
|
|
<NavigationDrawer /> // For mobile devices
|
|
```
|
|
|
|
### Key Features
|
|
|
|
#### 1. Conditional Rendering
|
|
|
|
- Different menus for authenticated vs unauthenticated users
|
|
- Notification badges for unread messages
|
|
- Responsive design for mobile/desktop
|
|
|
|
#### 2. Active State Management
|
|
|
|
```typescript
|
|
<NavLink
|
|
className={({ isActive }) =>
|
|
cn(
|
|
"px-2 py-2 rounded-2xl border flex flex-row",
|
|
isActive
|
|
? "bg-sidebar-accent text-sidebar-accent-foreground"
|
|
: "border-transparent hover:bg-sidebar-accent"
|
|
)
|
|
}
|
|
to={navLink.path}
|
|
>
|
|
```
|
|
|
|
#### 3. Collapsed State
|
|
|
|
- Icons only with tooltip hints
|
|
- Space-efficient for narrow screens
|
|
- Smooth transitions
|
|
|
|
#### 4. User Menu Integration
|
|
|
|
- Bottom section shows `UserMenu` component
|
|
- Profile/avatar display
|
|
- Settings and logout options
|
|
|
|
### Related Components
|
|
|
|
- `web/src/components/Navigation.tsx` - Main navigation logic
|
|
- `web/src/components/NavigationDrawer.tsx` - Mobile drawer implementation
|
|
- `web/src/components/UserMenu.tsx` - User profile dropdown
|
|
- `web/src/layouts/RootLayout.tsx` - Desktop layout with sidebar
|
|
- `web/src/components/MobileHeader.tsx` - Mobile header with drawer
|
|
|
|
### Internationalization
|
|
|
|
- Menu titles translated via `useTranslate()` hook
|
|
- Supports multiple languages
|
|
- Dynamic text based on user locale
|
|
|
|
The navigation system provides a clean, responsive menu that adapts to user authentication state and screen size while maintaining consistent UX across devices.
|
|
|
|
## Layout System (Masonry vs List)
|
|
|
|
### View Context Architecture
|
|
|
|
The layout system is managed through the `ViewContext` which provides global state management for layout preferences and sorting options.
|
|
|
|
### Core View Context (`web/src/contexts/ViewContext.tsx`)
|
|
|
|
```typescript
|
|
export type LayoutMode = "LIST" | "MASONRY";
|
|
|
|
interface ViewContextValue {
|
|
orderByTimeAsc: boolean; // Sort order
|
|
layout: LayoutMode; // Current layout mode
|
|
toggleSortOrder: () => void;
|
|
setLayout: (layout: LayoutMode) => void;
|
|
}
|
|
|
|
// Persistent storage in localStorage
|
|
const LOCAL_STORAGE_KEY = "memos-view-setting";
|
|
|
|
// Default state
|
|
return { orderByTimeAsc: false, layout: "LIST" as LayoutMode };
|
|
```
|
|
|
|
### Layout Modes
|
|
|
|
#### 1. LIST Layout (`"LIST"`)
|
|
|
|
- **Description**: Traditional linear list view
|
|
- **Implementation**: Single column layout
|
|
- **Behavior**: Memos displayed vertically in chronological order
|
|
- **Use Case**: Reading-focused, sequential browsing
|
|
|
|
#### 2. MASONRY Layout (`"MASONRY"`)
|
|
|
|
- **Description**: Pinterest-style grid layout
|
|
- **Implementation**: Multi-column responsive grid
|
|
- **Behavior**: Memos distributed based on actual rendered heights
|
|
- **Use Case**: Visual browsing, efficient space utilization
|
|
|
|
### Masonry Layout Implementation
|
|
|
|
#### Core Components
|
|
|
|
1. **MasonryView** (`web/src/components/MasonryView/MasonryView.tsx`)
|
|
- Main container component
|
|
- Manages column distribution
|
|
- Uses CSS Grid for layout
|
|
|
|
2. **MasonryColumn** (`web/src/components/MasonryView/MasonryColumn.tsx`)
|
|
- Represents individual columns
|
|
- Contains assigned memos
|
|
- Handles prefix elements (like memo editor)
|
|
|
|
3. **MasonryItem** (`web/src/components/MasonryView/MasonryItem.tsx`)
|
|
- Wraps individual memos
|
|
- Measures actual rendered height
|
|
- Uses ResizeObserver for dynamic updates
|
|
|
|
4. **useMasonryLayout** Hook (`web/src/components/MasonryView/useMasonryLayout.ts`)
|
|
- Calculates optimal column count
|
|
- Distributes memos to columns
|
|
- Manages height measurements
|
|
|
|
#### Key Features
|
|
|
|
##### 1. Height-Based Distribution
|
|
|
|
```typescript
|
|
// Smart algorithm that assigns memos to shortest column
|
|
const shortestColumnIndex = columnHeights.reduce(
|
|
(minIndex, currentHeight, currentIndex) =>
|
|
(currentHeight < columnHeights[minIndex] ? currentIndex : minIndex),
|
|
0
|
|
);
|
|
```
|
|
|
|
##### 2. Dynamic Column Count
|
|
|
|
```typescript
|
|
const calculateColumns = useCallback(() => {
|
|
if (!containerRef.current || listMode) return 1;
|
|
|
|
const containerWidth = containerRef.current.offsetWidth;
|
|
const scale = containerWidth / MINIMUM_MEMO_VIEWPORT_WIDTH;
|
|
return scale >= 1.2 ? Math.ceil(scale) : 1;
|
|
}, [containerRef, listMode]);
|
|
```
|
|
|
|
##### 3. ResizeObserver Integration
|
|
|
|
```typescript
|
|
useEffect(() => {
|
|
const measureHeight = () => {
|
|
if (itemRef.current) {
|
|
const height = itemRef.current.offsetHeight;
|
|
onHeightChange(memo.name, height);
|
|
}
|
|
};
|
|
|
|
resizeObserverRef.current = new ResizeObserver(measureHeight);
|
|
resizeObserverRef.current.observe(itemRef.current);
|
|
}, [memo.name, onHeightChange]);
|
|
```
|
|
|
|
##### 4. Responsive Behavior
|
|
|
|
- Automatically adjusts columns based on viewport width
|
|
- Minimum width threshold for multi-column layout
|
|
- Smooth transitions during resizing
|
|
|
|
### Layout Switching
|
|
|
|
#### User Interface
|
|
|
|
```typescript
|
|
// MemoDisplaySettingMenu component
|
|
<Select value={layout} onValueChange={(value) => setLayout(value as "LIST" | "MASONRY")}>
|
|
<SelectItem value="LIST">{t("memo.list")}</SelectItem>
|
|
<SelectItem value="MASONRY">{t("memo.masonry")}</SelectItem>
|
|
</Select>
|
|
```
|
|
|
|
#### State Management
|
|
|
|
```typescript
|
|
const { layout, setLayout } = useView();
|
|
|
|
// Toggle between layouts
|
|
setLayout("MASONRY"); // or "LIST"
|
|
|
|
// Persistence
|
|
localStorage.setItem("memos-view-setting", JSON.stringify({ layout, orderByTimeAsc }));
|
|
```
|
|
|
|
### Integration with Memo Display
|
|
|
|
#### PagedMemoList Component
|
|
|
|
```typescript
|
|
const PagedMemoList = (props: Props) => {
|
|
const { layout } = useView(); // Get current layout preference
|
|
|
|
return (
|
|
<div className="flex flex-col justify-start items-start w-full max-w-full">
|
|
{layout === "MASONRY" ? (
|
|
<MasonryView
|
|
memoList={sortedMemoList}
|
|
renderer={props.renderer}
|
|
prefixElement={<MemoEditor />}
|
|
/>
|
|
) : (
|
|
// Traditional list view implementation
|
|
<ListView memoList={sortedMemoList} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
### Performance Optimizations
|
|
|
|
1. **Debounced Redistribution**: Prevents excessive re-layout during rapid changes
|
|
2. **Memoized Calculations**: Optimized height calculations and distribution algorithms
|
|
3. **Efficient State Updates**: Only re-render when necessary
|
|
4. **ResizeObserver Cleanup**: Proper memory management
|
|
|
|
### Related Files
|
|
|
|
- `web/src/contexts/ViewContext.tsx` - Global layout state management
|
|
- `web/src/components/MasonryView/` - Masonry layout implementation
|
|
- `web/src/components/MemoDisplaySettingMenu.tsx` - Layout selection UI
|
|
- `web/src/components/PagedMemoList/PagedMemoList.tsx` - Memo list container
|
|
- `web/src/components/MasonryView/README.md` - Detailed technical documentation
|
|
|
|
### User Experience Benefits
|
|
|
|
- **Choice**: Users can choose preferred browsing style
|
|
- **Persistence**: Layout preference saved across sessions
|
|
- **Responsiveness**: Adapts to different screen sizes
|
|
- **Performance**: Optimized rendering for both modes
|
|
- **Consistency**: Same memo content, different presentation
|
|
|
|
The layout system provides flexible viewing options that cater to different user preferences and use cases while maintaining optimal performance and responsive design. |