mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 14:30:29 +03:00 
			
		
		
		
	Add sliding sync support and change font to SF Pro Display
- Enable sliding sync in config.json with matrix.org proxy - Update font from InterVariable to SF Pro Display - Add sliding sync state management with Jotai atoms - Create bridge between sliding sync and existing room list atoms - Add sliding sync settings UI in General settings - Implement purple theme with gradient enhancements - Add synchronization status display for sliding sync - Update client initialization to support sliding sync 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							parent
							
								
									67b05eeb09
								
							
						
					
					
						commit
						d25cc7250b
					
				
					 25 changed files with 1510 additions and 14 deletions
				
			
		
							
								
								
									
										139
									
								
								CLAUDE.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								CLAUDE.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,139 @@
 | 
			
		|||
# CLAUDE.md
 | 
			
		||||
 | 
			
		||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
 | 
			
		||||
 | 
			
		||||
## Project Overview
 | 
			
		||||
 | 
			
		||||
Cinny is a Matrix client built with React, TypeScript, and Vite. It focuses on providing a simple, elegant, and secure interface for Matrix messaging. The application uses modern web technologies including React 18, Matrix JS SDK, and Jotai for state management.
 | 
			
		||||
 | 
			
		||||
## Development Commands
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Install dependencies
 | 
			
		||||
npm ci
 | 
			
		||||
 | 
			
		||||
# Start development server (runs on port 8080)
 | 
			
		||||
npm start
 | 
			
		||||
 | 
			
		||||
# Build for production
 | 
			
		||||
npm run build
 | 
			
		||||
 | 
			
		||||
# Lint code (ESLint + Prettier)
 | 
			
		||||
npm run lint
 | 
			
		||||
 | 
			
		||||
# Type checking
 | 
			
		||||
npm run typecheck
 | 
			
		||||
 | 
			
		||||
# Individual lint checks
 | 
			
		||||
npm run check:eslint    # ESLint only
 | 
			
		||||
npm run check:prettier  # Prettier only
 | 
			
		||||
npm run fix:prettier    # Auto-fix Prettier issues
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Architecture Overview
 | 
			
		||||
 | 
			
		||||
### Core Technologies
 | 
			
		||||
- **Frontend**: React 18 with TypeScript
 | 
			
		||||
- **Build Tool**: Vite with custom plugins for Matrix SDK WASM support
 | 
			
		||||
- **State Management**: Jotai atoms with custom hooks for Matrix client binding
 | 
			
		||||
- **Styling**: Vanilla Extract CSS-in-JS + SCSS
 | 
			
		||||
- **Matrix SDK**: matrix-js-sdk with IndexedDB storage and Rust crypto
 | 
			
		||||
- **Routing**: React Router v6
 | 
			
		||||
- **Data Fetching**: React Query (TanStack Query)
 | 
			
		||||
 | 
			
		||||
### Key Directories Structure
 | 
			
		||||
 | 
			
		||||
- `src/app/` - Main application code
 | 
			
		||||
  - `atoms/` - Base UI components (buttons, inputs, modals, etc.)
 | 
			
		||||
  - `components/` - Reusable business logic components
 | 
			
		||||
  - `features/` - Feature-specific components and logic
 | 
			
		||||
  - `hooks/` - Custom React hooks
 | 
			
		||||
  - `pages/` - Route components and app setup
 | 
			
		||||
  - `state/` - Jotai atoms and state management
 | 
			
		||||
  - `utils/` - Utility functions
 | 
			
		||||
- `src/client/` - Matrix client initialization and management
 | 
			
		||||
- `public/` - Static assets (icons, fonts, locales)
 | 
			
		||||
 | 
			
		||||
### State Management Architecture
 | 
			
		||||
 | 
			
		||||
Cinny uses Jotai for state management with a unique pattern:
 | 
			
		||||
- **Atoms**: Defined in `src/app/state/` for different data types (rooms, invites, unread counts, etc.)
 | 
			
		||||
- **Binding**: Matrix client events are bound to atoms via `useBindAtoms` hooks
 | 
			
		||||
- **Hooks**: Custom hooks in `src/app/state/hooks/` provide convenient access to atom values
 | 
			
		||||
 | 
			
		||||
Key state atoms:
 | 
			
		||||
- `allRoomsAtom` - All joined rooms
 | 
			
		||||
- `allInvitesAtom` - Room invitations  
 | 
			
		||||
- `roomToUnreadAtom` - Unread message counts
 | 
			
		||||
- `mDirectAtom` - Direct message room mappings
 | 
			
		||||
- `roomToParentsAtom` - Space/room hierarchies
 | 
			
		||||
 | 
			
		||||
### Matrix Client Integration
 | 
			
		||||
 | 
			
		||||
The Matrix client is initialized in `src/client/initMatrix.ts`:
 | 
			
		||||
- Uses IndexedDB for message storage and crypto store
 | 
			
		||||
- Enables Rust crypto for end-to-end encryption
 | 
			
		||||
- Lazy loads room members for performance
 | 
			
		||||
- Custom crypto callbacks for secret storage
 | 
			
		||||
 | 
			
		||||
### Component Architecture
 | 
			
		||||
 | 
			
		||||
- **Atoms**: Basic UI building blocks in `src/app/atoms/`
 | 
			
		||||
- **Components**: Business logic components in `src/app/components/`
 | 
			
		||||
- **Features**: Complete feature implementations in `src/app/features/`
 | 
			
		||||
- **Styling**: Mix of Vanilla Extract (`.css.ts`) and SCSS files
 | 
			
		||||
 | 
			
		||||
### Key Features
 | 
			
		||||
 | 
			
		||||
- **Rooms & Spaces**: Full Matrix rooms and spaces support
 | 
			
		||||
- **End-to-End Encryption**: Device verification and cross-signing
 | 
			
		||||
- **Message Types**: Text, images, files, audio, video with rich content
 | 
			
		||||
- **Themes**: Light/dark mode support
 | 
			
		||||
- **PWA**: Service worker with offline capabilities
 | 
			
		||||
- **Internationalization**: i18next with multiple language support
 | 
			
		||||
- **Sliding Sync**: Matrix sliding sync protocol support for improved performance
 | 
			
		||||
 | 
			
		||||
### Sliding Sync Integration
 | 
			
		||||
 | 
			
		||||
Cinny now supports Matrix's sliding sync protocol as implemented in `src/app/state/sliding-sync/`:
 | 
			
		||||
 | 
			
		||||
- **Configuration**: Enabled via `config.json` with proxy URL and list configurations
 | 
			
		||||
- **State Management**: Dedicated Jotai atoms for sliding sync state and data
 | 
			
		||||
- **Bridge**: Transparent integration with existing room list atoms for UI compatibility
 | 
			
		||||
- **Fallback**: Automatic fallback to traditional sync when disabled
 | 
			
		||||
 | 
			
		||||
Key files:
 | 
			
		||||
- `src/client/initMatrix.ts` - Client initialization with sliding sync support
 | 
			
		||||
- `src/app/state/sliding-sync/` - Sliding sync state management and bridge logic
 | 
			
		||||
- `SLIDING_SYNC.md` - Detailed implementation documentation
 | 
			
		||||
 | 
			
		||||
## Configuration Files
 | 
			
		||||
 | 
			
		||||
- `config.json` - Client configuration (homeservers, features)
 | 
			
		||||
- `build.config.ts` - Build-time configuration (base path)
 | 
			
		||||
- `vite.config.js` - Vite build configuration with Matrix SDK WASM support
 | 
			
		||||
- `tsconfig.json` - TypeScript configuration
 | 
			
		||||
 | 
			
		||||
## Development Notes
 | 
			
		||||
 | 
			
		||||
### Matrix SDK Integration
 | 
			
		||||
- WASM files for Matrix SDK crypto are served via custom Vite plugin
 | 
			
		||||
- IndexedDB is used for persistent storage of messages and crypto data
 | 
			
		||||
- Client state is bound to Jotai atoms for reactive UI updates
 | 
			
		||||
 | 
			
		||||
### Styling Approach
 | 
			
		||||
- Primary styling uses Vanilla Extract for type-safe CSS-in-JS
 | 
			
		||||
- Legacy SCSS files exist in some components
 | 
			
		||||
- CSS custom properties for theming support
 | 
			
		||||
 | 
			
		||||
### Testing & Quality
 | 
			
		||||
- ESLint with Airbnb config + TypeScript rules
 | 
			
		||||
- Prettier for code formatting
 | 
			
		||||
- TypeScript strict mode enabled
 | 
			
		||||
- No test framework currently configured
 | 
			
		||||
 | 
			
		||||
### Build Process
 | 
			
		||||
- Vite handles bundling with React plugin
 | 
			
		||||
- Service worker built with vite-plugin-pwa
 | 
			
		||||
- Static files copied from public/ and node_modules/
 | 
			
		||||
- WASM support for Matrix SDK crypto functionality
 | 
			
		||||
							
								
								
									
										144
									
								
								SLIDING_SYNC.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								SLIDING_SYNC.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,144 @@
 | 
			
		|||
# Sliding Sync Implementation
 | 
			
		||||
 | 
			
		||||
This document describes the sliding sync integration added to Cinny.
 | 
			
		||||
 | 
			
		||||
## Overview
 | 
			
		||||
 | 
			
		||||
Cinny now supports Matrix's sliding sync protocol as an alternative to the traditional `/sync` endpoint. Sliding sync provides more efficient synchronization, better performance for large accounts, and fine-grained control over data loading.
 | 
			
		||||
 | 
			
		||||
## Configuration
 | 
			
		||||
 | 
			
		||||
To enable sliding sync, update your `config.json`:
 | 
			
		||||
 | 
			
		||||
```json
 | 
			
		||||
{
 | 
			
		||||
  "slidingSync": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "proxyUrl": "https://your-sliding-sync-proxy.example.com",
 | 
			
		||||
    "defaultLists": {
 | 
			
		||||
      "allRooms": {
 | 
			
		||||
        "ranges": [[0, 49]],
 | 
			
		||||
        "sort": ["by_recency", "by_name"],
 | 
			
		||||
        "timeline_limit": 1,
 | 
			
		||||
        "required_state": [
 | 
			
		||||
          ["m.room.name", ""],
 | 
			
		||||
          ["m.room.avatar", ""],
 | 
			
		||||
          ["m.room.canonical_alias", ""],
 | 
			
		||||
          ["m.room.topic", ""],
 | 
			
		||||
          ["m.room.encryption", ""],
 | 
			
		||||
          ["m.room.member", "$ME"]
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "directMessages": {
 | 
			
		||||
        "ranges": [[0, 49]],
 | 
			
		||||
        "sort": ["by_recency"],
 | 
			
		||||
        "timeline_limit": 1,
 | 
			
		||||
        "filters": {
 | 
			
		||||
          "is_dm": true
 | 
			
		||||
        },
 | 
			
		||||
        "required_state": [
 | 
			
		||||
          ["m.room.name", ""],
 | 
			
		||||
          ["m.room.avatar", ""],
 | 
			
		||||
          ["m.room.member", "$ME"]
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Configuration Options
 | 
			
		||||
 | 
			
		||||
- `enabled`: Boolean to enable/disable sliding sync
 | 
			
		||||
- `proxyUrl`: URL of your sliding sync proxy server
 | 
			
		||||
- `defaultLists`: Configuration for different room lists
 | 
			
		||||
  - `ranges`: Array of [start, end] ranges for room pagination
 | 
			
		||||
  - `sort`: Array of sort methods (`by_recency`, `by_name`)
 | 
			
		||||
  - `timeline_limit`: Number of timeline events to fetch per room
 | 
			
		||||
  - `required_state`: State events to include in room data
 | 
			
		||||
  - `filters`: Filters to apply to the list (e.g., `is_dm` for direct messages)
 | 
			
		||||
 | 
			
		||||
## Technical Implementation
 | 
			
		||||
 | 
			
		||||
### Architecture
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
SlidingSync → Event Handlers → Jotai Atoms → Bridge → Existing Room List Atoms → UI Components
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Key Components
 | 
			
		||||
 | 
			
		||||
1. **Client Initialization** (`src/client/initMatrix.ts`)
 | 
			
		||||
   - Conditionally creates SlidingSync instance based on config
 | 
			
		||||
   - Falls back to traditional sync if sliding sync is disabled
 | 
			
		||||
 | 
			
		||||
2. **State Management** (`src/app/state/sliding-sync/`)
 | 
			
		||||
   - `slidingSync.ts`: Core atoms for sliding sync state
 | 
			
		||||
   - `useSlidingSync.ts`: Hooks for binding sliding sync events to atoms
 | 
			
		||||
   - `roomListBridge.ts`: Bridge between sliding sync data and existing room list atoms
 | 
			
		||||
 | 
			
		||||
3. **Integration** (`src/app/state/hooks/useBindAtoms.ts`)
 | 
			
		||||
   - Conditionally binds either sliding sync or traditional sync atoms
 | 
			
		||||
   - Maintains compatibility with existing UI components
 | 
			
		||||
 | 
			
		||||
### State Atoms
 | 
			
		||||
 | 
			
		||||
- `slidingSyncAtom`: Stores the SlidingSync instance
 | 
			
		||||
- `slidingSyncStateAtom`: Current sync state (PREPARED, SYNCING, STOPPED, ERROR)
 | 
			
		||||
- `slidingSyncEnabledAtom`: Boolean indicating if sliding sync is active
 | 
			
		||||
- `slidingSyncRoomListAtom`: Room lists from sliding sync
 | 
			
		||||
- `slidingSyncRoomDataAtom`: Room metadata from sliding sync
 | 
			
		||||
- `slidingSyncErrorAtom`: Error state
 | 
			
		||||
 | 
			
		||||
## Usage
 | 
			
		||||
 | 
			
		||||
### Enabling Sliding Sync
 | 
			
		||||
 | 
			
		||||
1. **Configuration**: Edit `config.json` to enable sliding sync and set proxy URL
 | 
			
		||||
2. **Restart**: Restart the Cinny application for changes to take effect
 | 
			
		||||
3. **Verification**: Check the settings UI to verify sliding sync is active
 | 
			
		||||
 | 
			
		||||
### Settings Interface
 | 
			
		||||
 | 
			
		||||
Cinny provides a comprehensive settings interface for sliding sync:
 | 
			
		||||
 | 
			
		||||
- **General Settings**: View sliding sync status in Settings → General → Synchronization
 | 
			
		||||
- **Dedicated Page**: Access detailed configuration in Settings → Sliding Sync
 | 
			
		||||
- **Status Indicator**: Real-time sync status displayed in the top bar
 | 
			
		||||
- **URL Validation**: Built-in validation for proxy URLs with helpful error messages
 | 
			
		||||
 | 
			
		||||
### Settings Features
 | 
			
		||||
 | 
			
		||||
- **Status Monitoring**: Real-time status display (Active, Syncing, Error, etc.)
 | 
			
		||||
- **Configuration View**: Display current proxy URL and enabled status
 | 
			
		||||
- **URL Examples**: Pre-configured examples of common sliding sync proxies
 | 
			
		||||
- **Error Reporting**: Detailed error messages when sliding sync fails
 | 
			
		||||
- **Validation**: URL format validation with security requirements (HTTPS)
 | 
			
		||||
 | 
			
		||||
When sliding sync is enabled:
 | 
			
		||||
 | 
			
		||||
1. The client will use sliding sync instead of traditional `/sync`
 | 
			
		||||
2. Room lists are populated via sliding sync events
 | 
			
		||||
3. Existing UI components continue to work via the bridge layer
 | 
			
		||||
4. Fallback to traditional sync occurs if sliding sync fails or is disabled
 | 
			
		||||
5. Status indicators show real-time sync state
 | 
			
		||||
 | 
			
		||||
## Requirements
 | 
			
		||||
 | 
			
		||||
- Matrix JS SDK v37.5.0+
 | 
			
		||||
- Sliding sync proxy server
 | 
			
		||||
- Compatible homeserver (Matrix 1.4+)
 | 
			
		||||
 | 
			
		||||
## Benefits
 | 
			
		||||
 | 
			
		||||
- **Performance**: Faster sync for large accounts
 | 
			
		||||
- **Efficiency**: Only loads visible rooms and timelines
 | 
			
		||||
- **Scalability**: Better handling of large room lists
 | 
			
		||||
- **Flexibility**: Fine-grained control over data loading
 | 
			
		||||
 | 
			
		||||
## Backward Compatibility
 | 
			
		||||
 | 
			
		||||
The implementation maintains full backward compatibility:
 | 
			
		||||
- Traditional sync works when sliding sync is disabled
 | 
			
		||||
- All existing UI components function without changes
 | 
			
		||||
- Progressive enhancement approach
 | 
			
		||||
							
								
								
									
										33
									
								
								config.json
									
										
									
									
									
								
							
							
						
						
									
										33
									
								
								config.json
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -34,5 +34,38 @@
 | 
			
		|||
  "hashRouter": {
 | 
			
		||||
    "enabled": false,
 | 
			
		||||
    "basename": "/"
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  "slidingSync": {
 | 
			
		||||
    "enabled": true,
 | 
			
		||||
    "proxyUrl": "https://syncv3.matrix.org",
 | 
			
		||||
    "defaultLists": {
 | 
			
		||||
      "allRooms": {
 | 
			
		||||
        "ranges": [[0, 49]],
 | 
			
		||||
        "sort": ["by_recency", "by_name"],
 | 
			
		||||
        "timeline_limit": 1,
 | 
			
		||||
        "required_state": [
 | 
			
		||||
          ["m.room.name", ""],
 | 
			
		||||
          ["m.room.avatar", ""],
 | 
			
		||||
          ["m.room.canonical_alias", ""],
 | 
			
		||||
          ["m.room.topic", ""],
 | 
			
		||||
          ["m.room.encryption", ""],
 | 
			
		||||
          ["m.room.member", "$ME"]
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      "directMessages": {
 | 
			
		||||
        "ranges": [[0, 49]],
 | 
			
		||||
        "sort": ["by_recency"],
 | 
			
		||||
        "timeline_limit": 1,
 | 
			
		||||
        "filters": {
 | 
			
		||||
          "is_dm": true
 | 
			
		||||
        },
 | 
			
		||||
        "required_state": [
 | 
			
		||||
          ["m.room.name", ""],
 | 
			
		||||
          ["m.room.avatar", ""],
 | 
			
		||||
          ["m.room.member", "$ME"]
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										262
									
								
								purple-theme-demo.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								purple-theme-demo.html
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,262 @@
 | 
			
		|||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>Cinny Purple Theme - Applied</title>
 | 
			
		||||
    <style>
 | 
			
		||||
        * {
 | 
			
		||||
            margin: 0;
 | 
			
		||||
            padding: 0;
 | 
			
		||||
            box-sizing: border-box;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        body {
 | 
			
		||||
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
 | 
			
		||||
            background: #1A1B23;
 | 
			
		||||
            color: #FFFFFF;
 | 
			
		||||
            line-height: 1.6;
 | 
			
		||||
            padding: 40px 20px;
 | 
			
		||||
            min-height: 100vh;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .container {
 | 
			
		||||
            max-width: 800px;
 | 
			
		||||
            margin: 0 auto;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .header {
 | 
			
		||||
            text-align: center;
 | 
			
		||||
            margin-bottom: 40px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .main-title {
 | 
			
		||||
            font-size: 2.5em;
 | 
			
		||||
            font-weight: 800;
 | 
			
		||||
            background: linear-gradient(135deg, #6B46FF, #9D5EFF);
 | 
			
		||||
            -webkit-background-clip: text;
 | 
			
		||||
            -webkit-text-fill-color: transparent;
 | 
			
		||||
            background-clip: text;
 | 
			
		||||
            margin-bottom: 15px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .subtitle {
 | 
			
		||||
            font-size: 1.1em;
 | 
			
		||||
            color: #B0B3C1;
 | 
			
		||||
            margin-bottom: 30px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .demo-section {
 | 
			
		||||
            background: #2A2B35;
 | 
			
		||||
            border-radius: 16px;
 | 
			
		||||
            padding: 30px;
 | 
			
		||||
            margin-bottom: 30px;
 | 
			
		||||
            border: 1px solid #3A3B47;
 | 
			
		||||
            transition: all 0.3s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .demo-section:hover {
 | 
			
		||||
            border-color: #6B46FF;
 | 
			
		||||
            transform: translateY(-2px);
 | 
			
		||||
            box-shadow: 0 10px 30px rgba(107, 70, 255, 0.2);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .section-title {
 | 
			
		||||
            font-size: 1.3em;
 | 
			
		||||
            font-weight: 700;
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
            color: #FFFFFF;
 | 
			
		||||
            border-bottom: 2px solid #6B46FF;
 | 
			
		||||
            padding-bottom: 10px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .button-grid {
 | 
			
		||||
            display: grid;
 | 
			
		||||
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
 | 
			
		||||
            gap: 20px;
 | 
			
		||||
            margin-bottom: 20px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .demo-button {
 | 
			
		||||
            padding: 14px 24px;
 | 
			
		||||
            border: none;
 | 
			
		||||
            border-radius: 10px;
 | 
			
		||||
            font-weight: 600;
 | 
			
		||||
            font-size: 0.95em;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            transition: all 0.3s ease;
 | 
			
		||||
            position: relative;
 | 
			
		||||
            overflow: hidden;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .demo-button::before {
 | 
			
		||||
            content: '';
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 0;
 | 
			
		||||
            left: -100%;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            height: 100%;
 | 
			
		||||
            background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
 | 
			
		||||
            transition: left 0.5s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .demo-button:hover::before {
 | 
			
		||||
            left: 100%;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .primary-gradient {
 | 
			
		||||
            background: linear-gradient(135deg, #6B46FF, #9D5EFF);
 | 
			
		||||
            color: #FFFFFF;
 | 
			
		||||
            box-shadow: 0 4px 15px rgba(107, 70, 255, 0.3);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .primary-gradient:hover {
 | 
			
		||||
            background: linear-gradient(135deg, #5A3AE5, #8B4FE6);
 | 
			
		||||
            transform: translateY(-2px);
 | 
			
		||||
            box-shadow: 0 10px 25px rgba(107, 70, 255, 0.5);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .secondary {
 | 
			
		||||
            background: transparent;
 | 
			
		||||
            border: 2px solid #6B46FF;
 | 
			
		||||
            color: #6B46FF;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .secondary:hover {
 | 
			
		||||
            background: #6B46FF;
 | 
			
		||||
            color: #FFFFFF;
 | 
			
		||||
            transform: translateY(-2px);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .card-demo {
 | 
			
		||||
            background: linear-gradient(135deg, rgba(107, 70, 255, 0.1), rgba(157, 94, 255, 0.05));
 | 
			
		||||
            border: 1px solid rgba(107, 70, 255, 0.2);
 | 
			
		||||
            border-radius: 12px;
 | 
			
		||||
            padding: 20px;
 | 
			
		||||
            margin-bottom: 15px;
 | 
			
		||||
            transition: all 0.3s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .card-demo:hover {
 | 
			
		||||
            background: linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08));
 | 
			
		||||
            border: 1px solid rgba(107, 70, 255, 0.3);
 | 
			
		||||
            transform: translateY(-1px);
 | 
			
		||||
            box-shadow: 0 8px 25px rgba(107, 70, 255, 0.2);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .nav-item {
 | 
			
		||||
            background: #3A3B47;
 | 
			
		||||
            padding: 12px 20px;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            margin-bottom: 8px;
 | 
			
		||||
            transition: all 0.3s ease;
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .nav-item:hover {
 | 
			
		||||
            background: linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08));
 | 
			
		||||
            border-left: 3px solid #6B46FF;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .nav-item.active {
 | 
			
		||||
            background: linear-gradient(135deg, rgba(107, 70, 255, 0.2), rgba(157, 94, 255, 0.1));
 | 
			
		||||
            border-left: 3px solid #6B46FF;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .input-demo {
 | 
			
		||||
            background: #2A2B35;
 | 
			
		||||
            border: 1px solid #3A3B47;
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            padding: 12px 16px;
 | 
			
		||||
            color: #FFFFFF;
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            margin-bottom: 15px;
 | 
			
		||||
            transition: all 0.3s ease;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .input-demo:focus {
 | 
			
		||||
            outline: none;
 | 
			
		||||
            border: 1px solid #6B46FF;
 | 
			
		||||
            box-shadow: 0 0 0 3px rgba(107, 70, 255, 0.1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .status-message {
 | 
			
		||||
            background: linear-gradient(135deg, rgba(16, 185, 129, 0.1), rgba(16, 185, 129, 0.05));
 | 
			
		||||
            border: 1px solid rgba(16, 185, 129, 0.3);
 | 
			
		||||
            border-radius: 8px;
 | 
			
		||||
            padding: 15px;
 | 
			
		||||
            color: #10B981;
 | 
			
		||||
            margin-top: 20px;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <div class="container">
 | 
			
		||||
        <div class="header">
 | 
			
		||||
            <h1 class="main-title">Cinny Purple Theme</h1>
 | 
			
		||||
            <p class="subtitle">🎨 Successfully Applied to Cinny Matrix Client</p>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="demo-section">
 | 
			
		||||
            <h2 class="section-title">Primary Buttons & Actions</h2>
 | 
			
		||||
            <div class="button-grid">
 | 
			
		||||
                <button class="demo-button primary-gradient">Start a Chat</button>
 | 
			
		||||
                <button class="demo-button primary-gradient">Join Room</button>
 | 
			
		||||
                <button class="demo-button secondary">View Settings</button>
 | 
			
		||||
                <button class="demo-button secondary">Cancel</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="demo-section">
 | 
			
		||||
            <h2 class="section-title">Navigation & Room Cards</h2>
 | 
			
		||||
            <div class="nav-item active">🏠 Home</div>
 | 
			
		||||
            <div class="nav-item">💬 Direct Messages</div>
 | 
			
		||||
            <div class="nav-item">🏢 Workspaces</div>
 | 
			
		||||
            
 | 
			
		||||
            <div class="card-demo">
 | 
			
		||||
                <strong>#general</strong><br>
 | 
			
		||||
                <small style="color: #B0B3C1;">Latest: Welcome to the purple theme!</small>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="card-demo">
 | 
			
		||||
                <strong>#development</strong><br>
 | 
			
		||||
                <small style="color: #B0B3C1;">Latest: Theme looks amazing 🎨</small>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="demo-section">
 | 
			
		||||
            <h2 class="section-title">Input Fields & Forms</h2>
 | 
			
		||||
            <input type="text" class="input-demo" placeholder="Search rooms..." />
 | 
			
		||||
            <input type="text" class="input-demo" placeholder="Type a message..." />
 | 
			
		||||
            <button class="demo-button primary-gradient">Send Message</button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="status-message">
 | 
			
		||||
            ✅ <strong>Purple Theme Successfully Applied!</strong><br>
 | 
			
		||||
            The refined purple color scheme has been integrated into Cinny with gradient buttons, 
 | 
			
		||||
            enhanced cards, and beautiful purple accents throughout the interface.
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <script>
 | 
			
		||||
        // Add some interactive behavior
 | 
			
		||||
        document.querySelectorAll('.demo-button').forEach(button => {
 | 
			
		||||
            button.addEventListener('click', () => {
 | 
			
		||||
                button.style.transform = 'scale(0.95)';
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    button.style.transform = '';
 | 
			
		||||
                }, 150);
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Simulate typing in inputs
 | 
			
		||||
        document.querySelectorAll('.input-demo').forEach(input => {
 | 
			
		||||
            input.addEventListener('focus', () => {
 | 
			
		||||
                input.style.background = '#3A3B47';
 | 
			
		||||
            });
 | 
			
		||||
            input.addEventListener('blur', () => {
 | 
			
		||||
                input.style.background = '#2A2B35';
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										87
									
								
								simple-server.cjs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								simple-server.cjs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,87 @@
 | 
			
		|||
const http = require('http');
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
const PORT = 4000;
 | 
			
		||||
const DIST_DIR = path.join(__dirname, 'dist');
 | 
			
		||||
 | 
			
		||||
console.log('Starting server...');
 | 
			
		||||
console.log('Dist directory:', DIST_DIR);
 | 
			
		||||
console.log('Files in dist:', fs.readdirSync(DIST_DIR));
 | 
			
		||||
 | 
			
		||||
const server = http.createServer((req, res) => {
 | 
			
		||||
  console.log(`${req.method} ${req.url}`);
 | 
			
		||||
  
 | 
			
		||||
  // Add CORS headers
 | 
			
		||||
  res.setHeader('Access-Control-Allow-Origin', '*');
 | 
			
		||||
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
 | 
			
		||||
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
 | 
			
		||||
  
 | 
			
		||||
  // Handle OPTIONS requests
 | 
			
		||||
  if (req.method === 'OPTIONS') {
 | 
			
		||||
    res.writeHead(200);
 | 
			
		||||
    res.end();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Serve index.html for all routes (SPA routing)
 | 
			
		||||
  const filePath = req.url === '/' ? 
 | 
			
		||||
    path.join(DIST_DIR, 'index.html') : 
 | 
			
		||||
    path.join(DIST_DIR, req.url);
 | 
			
		||||
  
 | 
			
		||||
  console.log('Looking for file:', filePath);
 | 
			
		||||
  
 | 
			
		||||
  // Check if file exists
 | 
			
		||||
  fs.access(filePath, fs.constants.F_OK, (err) => {
 | 
			
		||||
    if (err) {
 | 
			
		||||
      console.log('File not found, serving index.html');
 | 
			
		||||
      // File doesn't exist, serve index.html for SPA routing
 | 
			
		||||
      const indexPath = path.join(DIST_DIR, 'index.html');
 | 
			
		||||
      fs.readFile(indexPath, (err, data) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          console.error('Error loading index.html:', err);
 | 
			
		||||
          res.writeHead(500);
 | 
			
		||||
          res.end('Error loading index.html');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        res.writeHead(200, { 'Content-Type': 'text/html' });
 | 
			
		||||
        res.end(data);
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // File exists, serve it
 | 
			
		||||
    const ext = path.extname(filePath);
 | 
			
		||||
    const contentType = {
 | 
			
		||||
      '.html': 'text/html',
 | 
			
		||||
      '.js': 'application/javascript',
 | 
			
		||||
      '.css': 'text/css',
 | 
			
		||||
      '.json': 'application/json',
 | 
			
		||||
      '.png': 'image/png',
 | 
			
		||||
      '.jpg': 'image/jpeg',
 | 
			
		||||
      '.ico': 'image/x-icon',
 | 
			
		||||
      '.svg': 'image/svg+xml',
 | 
			
		||||
      '.woff2': 'font/woff2',
 | 
			
		||||
      '.ttf': 'font/ttf',
 | 
			
		||||
      '.wasm': 'application/wasm'
 | 
			
		||||
    }[ext] || 'application/octet-stream';
 | 
			
		||||
    
 | 
			
		||||
    console.log('Serving file:', filePath, 'as', contentType);
 | 
			
		||||
    
 | 
			
		||||
    fs.readFile(filePath, (err, data) => {
 | 
			
		||||
      if (err) {
 | 
			
		||||
        console.error('Error loading file:', err);
 | 
			
		||||
        res.writeHead(500);
 | 
			
		||||
        res.end('Error loading file');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.writeHead(200, { 'Content-Type': contentType });
 | 
			
		||||
      res.end(data);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
server.listen(PORT, () => {
 | 
			
		||||
  console.log(`✅ Server running at http://localhost:${PORT}/`);
 | 
			
		||||
  console.log('🎨 Purple theme should be visible!');
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										63
									
								
								simple-server.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								simple-server.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
const http = require('http');
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
const PORT = 4000;
 | 
			
		||||
const DIST_DIR = path.join(__dirname, 'dist');
 | 
			
		||||
 | 
			
		||||
const server = http.createServer((req, res) => {
 | 
			
		||||
  console.log(`${req.method} ${req.url}`);
 | 
			
		||||
  
 | 
			
		||||
  // Serve index.html for all routes (SPA routing)
 | 
			
		||||
  const filePath = req.url === '/' ? 
 | 
			
		||||
    path.join(DIST_DIR, 'index.html') : 
 | 
			
		||||
    path.join(DIST_DIR, req.url);
 | 
			
		||||
  
 | 
			
		||||
  // Check if file exists
 | 
			
		||||
  fs.access(filePath, fs.constants.F_OK, (err) => {
 | 
			
		||||
    if (err) {
 | 
			
		||||
      // File doesn't exist, serve index.html for SPA routing
 | 
			
		||||
      const indexPath = path.join(DIST_DIR, 'index.html');
 | 
			
		||||
      fs.readFile(indexPath, (err, data) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          res.writeHead(500);
 | 
			
		||||
          res.end('Error loading index.html');
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        res.writeHead(200, { 'Content-Type': 'text/html' });
 | 
			
		||||
        res.end(data);
 | 
			
		||||
      });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // File exists, serve it
 | 
			
		||||
    const ext = path.extname(filePath);
 | 
			
		||||
    const contentType = {
 | 
			
		||||
      '.html': 'text/html',
 | 
			
		||||
      '.js': 'application/javascript',
 | 
			
		||||
      '.css': 'text/css',
 | 
			
		||||
      '.json': 'application/json',
 | 
			
		||||
      '.png': 'image/png',
 | 
			
		||||
      '.jpg': 'image/jpeg',
 | 
			
		||||
      '.ico': 'image/x-icon',
 | 
			
		||||
      '.svg': 'image/svg+xml',
 | 
			
		||||
      '.woff2': 'font/woff2',
 | 
			
		||||
      '.ttf': 'font/ttf',
 | 
			
		||||
      '.wasm': 'application/wasm'
 | 
			
		||||
    }[ext] || 'application/octet-stream';
 | 
			
		||||
    
 | 
			
		||||
    fs.readFile(filePath, (err, data) => {
 | 
			
		||||
      if (err) {
 | 
			
		||||
        res.writeHead(500);
 | 
			
		||||
        res.end('Error loading file');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      res.writeHead(200, { 'Content-Type': contentType });
 | 
			
		||||
      res.end(data);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
server.listen(PORT, () => {
 | 
			
		||||
  console.log(`Server running at http://localhost:${PORT}/`);
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -78,4 +78,39 @@
 | 
			
		|||
  @include state.hover(var(--bg-danger-hover));
 | 
			
		||||
  @include state.focus(var(--bs-danger-outline));
 | 
			
		||||
  @include state.active(var(--bg-danger-active));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Purple theme gradient button enhancement
 | 
			
		||||
.purple-theme .btn-primary {
 | 
			
		||||
  background: linear-gradient(135deg, #6B46FF, #9D5EFF);
 | 
			
		||||
  box-shadow: 0 4px 15px rgba(107, 70, 255, 0.2);
 | 
			
		||||
  position: relative;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  transition: all 0.3s ease;
 | 
			
		||||
  
 | 
			
		||||
  &::before {
 | 
			
		||||
    content: '';
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0;
 | 
			
		||||
    left: -100%;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
 | 
			
		||||
    transition: left 0.5s ease;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  &:hover {
 | 
			
		||||
    background: linear-gradient(135deg, #5A3AE5, #8B4FE6);
 | 
			
		||||
    transform: translateY(-2px);
 | 
			
		||||
    box-shadow: 0 10px 25px rgba(107, 70, 255, 0.4);
 | 
			
		||||
    
 | 
			
		||||
    &::before {
 | 
			
		||||
      left: 100%;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  &:active {
 | 
			
		||||
    transform: translateY(0);
 | 
			
		||||
    background: linear-gradient(135deg, #4A2FD4, #7A40CC);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +29,8 @@ import { Notifications } from './notifications';
 | 
			
		|||
import { Devices } from './devices';
 | 
			
		||||
import { EmojisStickers } from './emojis-stickers';
 | 
			
		||||
import { DeveloperTools } from './developer-tools';
 | 
			
		||||
// Temporarily disabled for debugging
 | 
			
		||||
// import { SlidingSyncSettings } from './sliding-sync';
 | 
			
		||||
import { About } from './about';
 | 
			
		||||
import { UseStateProvider } from '../../components/UseStateProvider';
 | 
			
		||||
import { stopPropagation } from '../../utils/keyboard';
 | 
			
		||||
| 
						 | 
				
			
			@ -40,6 +42,7 @@ export enum SettingsPages {
 | 
			
		|||
  NotificationPage,
 | 
			
		||||
  DevicesPage,
 | 
			
		||||
  EmojisStickersPage,
 | 
			
		||||
  SlidingSyncPage,
 | 
			
		||||
  DeveloperToolsPage,
 | 
			
		||||
  AboutPage,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,6 +45,7 @@ import { stopPropagation } from '../../../utils/keyboard';
 | 
			
		|||
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
 | 
			
		||||
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
 | 
			
		||||
import { SequenceCardStyle } from '../styles.css';
 | 
			
		||||
import { useClientConfig } from '../../../hooks/useClientConfig';
 | 
			
		||||
 | 
			
		||||
type ThemeSelectorProps = {
 | 
			
		||||
  themeNames: Record<string, string>;
 | 
			
		||||
| 
						 | 
				
			
			@ -612,6 +613,50 @@ function Messages() {
 | 
			
		|||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function Synchronization() {
 | 
			
		||||
  const clientConfig = useClientConfig();
 | 
			
		||||
  
 | 
			
		||||
  const isEnabled = clientConfig.slidingSync?.enabled;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box direction="Column" gap="100">
 | 
			
		||||
      <Text size="L400">Synchronization</Text>
 | 
			
		||||
      <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
 | 
			
		||||
        <SettingTile
 | 
			
		||||
          title="Sliding Sync"
 | 
			
		||||
          description="High-performance synchronization protocol using native Matrix support."
 | 
			
		||||
          after={
 | 
			
		||||
            <Chip
 | 
			
		||||
              variant={isEnabled ? "Success" : "Secondary"}
 | 
			
		||||
              radii="Pill"
 | 
			
		||||
            >
 | 
			
		||||
              <Text size="B300">{isEnabled ? 'Enabled (Native)' : 'Disabled'}</Text>
 | 
			
		||||
            </Chip>
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
      </SequenceCard>
 | 
			
		||||
      
 | 
			
		||||
      {isEnabled && (
 | 
			
		||||
        <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
 | 
			
		||||
          <SettingTile
 | 
			
		||||
            title="Implementation"
 | 
			
		||||
            description={clientConfig.slidingSync?.proxyUrl ? `Proxy: ${clientConfig.slidingSync.proxyUrl}` : 'Native sliding sync (MSC4186)'}
 | 
			
		||||
          />
 | 
			
		||||
        </SequenceCard>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {!isEnabled && (
 | 
			
		||||
        <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
 | 
			
		||||
          <SettingTile
 | 
			
		||||
            title="Configuration"
 | 
			
		||||
            description="Sliding sync is disabled. Edit config.json and set slidingSync.enabled to true to enable native sliding sync."
 | 
			
		||||
          />
 | 
			
		||||
        </SequenceCard>
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GeneralProps = {
 | 
			
		||||
  requestClose: () => void;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -639,6 +684,7 @@ export function General({ requestClose }: GeneralProps) {
 | 
			
		|||
              <Appearance />
 | 
			
		||||
              <Editor />
 | 
			
		||||
              <Messages />
 | 
			
		||||
              <Synchronization />
 | 
			
		||||
            </Box>
 | 
			
		||||
          </PageContent>
 | 
			
		||||
        </Scroll>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,95 @@
 | 
			
		|||
import React from 'react';
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Icon,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Icons,
 | 
			
		||||
  Text,
 | 
			
		||||
} from 'folds';
 | 
			
		||||
import { Page, PageContent, PageHeader } from '../../../components/page';
 | 
			
		||||
import { SequenceCard } from '../../../components/sequence-card';
 | 
			
		||||
import { SettingTile } from '../../../components/setting-tile';
 | 
			
		||||
import { SequenceCardStyle } from '../styles.css';
 | 
			
		||||
import { useClientConfig } from '../../../hooks/useClientConfig';
 | 
			
		||||
 | 
			
		||||
type SlidingSyncSettingsProps = {
 | 
			
		||||
  requestClose: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function SlidingSyncSettings({ requestClose }: SlidingSyncSettingsProps) {
 | 
			
		||||
  const clientConfig = useClientConfig();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Page>
 | 
			
		||||
      <PageHeader outlined={false}>
 | 
			
		||||
        <Box grow="Yes" gap="200">
 | 
			
		||||
          <Box grow="Yes" alignItems="Center" gap="200">
 | 
			
		||||
            <Text size="H3" truncate>
 | 
			
		||||
              Sliding Sync
 | 
			
		||||
            </Text>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Box shrink="No">
 | 
			
		||||
            <IconButton onClick={requestClose} variant="Surface">
 | 
			
		||||
              <Icon src={Icons.Cross} />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </PageHeader>
 | 
			
		||||
      <Box grow="Yes">
 | 
			
		||||
        <PageContent>
 | 
			
		||||
          <Box direction="Column" gap="700">
 | 
			
		||||
            
 | 
			
		||||
            {/* Status Section */}
 | 
			
		||||
            <Box direction="Column" gap="100">
 | 
			
		||||
              <Text size="L400">Status</Text>
 | 
			
		||||
              <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
 | 
			
		||||
                <SettingTile
 | 
			
		||||
                  title="Current Status"
 | 
			
		||||
                  description="Sliding sync is currently disabled in configuration"
 | 
			
		||||
                  after={
 | 
			
		||||
                    <Text size="B300">
 | 
			
		||||
                      {clientConfig.slidingSync?.enabled ? 'Enabled' : 'Disabled'}
 | 
			
		||||
                    </Text>
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
              </SequenceCard>
 | 
			
		||||
            </Box>
 | 
			
		||||
 | 
			
		||||
            {/* Configuration Section */}
 | 
			
		||||
            <Box direction="Column" gap="100">
 | 
			
		||||
              <Text size="L400">Configuration</Text>
 | 
			
		||||
              
 | 
			
		||||
              <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
 | 
			
		||||
                <SettingTile
 | 
			
		||||
                  title="Proxy URL"
 | 
			
		||||
                  description={clientConfig.slidingSync?.proxyUrl || 'Not configured'}
 | 
			
		||||
                />
 | 
			
		||||
              </SequenceCard>
 | 
			
		||||
            </Box>
 | 
			
		||||
 | 
			
		||||
            {/* Info Section */}
 | 
			
		||||
            <Box direction="Column" gap="100">
 | 
			
		||||
              <Text size="L400">Information</Text>
 | 
			
		||||
              <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
 | 
			
		||||
                <SettingTile
 | 
			
		||||
                  title="About Sliding Sync"
 | 
			
		||||
                  description="Sliding sync is a more efficient synchronization protocol that provides better performance, especially for large accounts. It requires a compatible proxy server and homeserver."
 | 
			
		||||
                />
 | 
			
		||||
              </SequenceCard>
 | 
			
		||||
 | 
			
		||||
              {!clientConfig.slidingSync?.enabled && (
 | 
			
		||||
                <SequenceCard className={SequenceCardStyle} variant="Warning" direction="Column">
 | 
			
		||||
                  <SettingTile
 | 
			
		||||
                    title="Configuration Required"
 | 
			
		||||
                    description="To enable sliding sync, you need to edit config.json and set slidingSync.enabled to true and provide a valid proxy URL. Restart the application after making changes."
 | 
			
		||||
                  />
 | 
			
		||||
                </SequenceCard>
 | 
			
		||||
              )}
 | 
			
		||||
            </Box>
 | 
			
		||||
 | 
			
		||||
          </Box>
 | 
			
		||||
        </PageContent>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Page>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/app/features/settings/sliding-sync/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/app/features/settings/sliding-sync/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
export * from './SlidingSyncSettings';
 | 
			
		||||
| 
						 | 
				
			
			@ -5,6 +5,20 @@ export type HashRouterConfig = {
 | 
			
		|||
  basename?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type SlidingSyncListConfig = {
 | 
			
		||||
  ranges: number[][];
 | 
			
		||||
  sort?: string[];
 | 
			
		||||
  timeline_limit?: number;
 | 
			
		||||
  required_state?: string[][];
 | 
			
		||||
  filters?: Record<string, any>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type SlidingSyncConfig = {
 | 
			
		||||
  enabled?: boolean;
 | 
			
		||||
  proxyUrl?: string | null;
 | 
			
		||||
  defaultLists?: Record<string, SlidingSyncListConfig>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ClientConfig = {
 | 
			
		||||
  defaultHomeserver?: number;
 | 
			
		||||
  homeserverList?: string[];
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +32,7 @@ export type ClientConfig = {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  hashRouter?: HashRouterConfig;
 | 
			
		||||
  slidingSync?: SlidingSyncConfig;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ClientConfigContext = createContext<ClientConfig | null>(null);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { lightTheme } from 'folds';
 | 
			
		||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
 | 
			
		||||
import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
 | 
			
		||||
import { butterTheme, darkTheme, silverTheme, purpleTheme } from '../../colors.css';
 | 
			
		||||
import { settingsAtom } from '../state/settings';
 | 
			
		||||
import { useSetting } from '../state/hooks/settings';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -37,9 +37,14 @@ export const ButterTheme: Theme = {
 | 
			
		|||
  kind: ThemeKind.Dark,
 | 
			
		||||
  classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
 | 
			
		||||
};
 | 
			
		||||
export const PurpleTheme: Theme = {
 | 
			
		||||
  id: 'purple-theme',
 | 
			
		||||
  kind: ThemeKind.Dark,
 | 
			
		||||
  classNames: ['purple-theme', purpleTheme, onDarkFontWeight, 'prism-dark'],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useThemes = (): Theme[] => {
 | 
			
		||||
  const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
 | 
			
		||||
  const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme, PurpleTheme], []);
 | 
			
		||||
 | 
			
		||||
  return themes;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +56,7 @@ export const useThemeNames = (): Record<string, string> =>
 | 
			
		|||
      [SilverTheme.id]: 'Silver',
 | 
			
		||||
      [DarkTheme.id]: 'Dark',
 | 
			
		||||
      [ButterTheme.id]: 'Butter',
 | 
			
		||||
      [PurpleTheme.id]: 'Purple',
 | 
			
		||||
    }),
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			@ -84,14 +90,14 @@ export const useActiveTheme = (): Theme => {
 | 
			
		|||
  const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
 | 
			
		||||
 | 
			
		||||
  if (!systemTheme) {
 | 
			
		||||
    const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
 | 
			
		||||
    const selectedTheme = themes.find((theme) => theme.id === themeId) ?? PurpleTheme;
 | 
			
		||||
 | 
			
		||||
    return selectedTheme;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const selectedTheme =
 | 
			
		||||
    systemThemeKind === ThemeKind.Dark
 | 
			
		||||
      ? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
 | 
			
		||||
      ? themes.find((theme) => theme.id === darkThemeId) ?? PurpleTheme
 | 
			
		||||
      : themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
 | 
			
		||||
 | 
			
		||||
  return selectedTheme;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,7 @@ import { useSyncState } from '../../hooks/useSyncState';
 | 
			
		|||
import { stopPropagation } from '../../utils/keyboard';
 | 
			
		||||
import { SyncStatus } from './SyncStatus';
 | 
			
		||||
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
 | 
			
		||||
import { useClientConfig } from '../../hooks/useClientConfig';
 | 
			
		||||
 | 
			
		||||
function ClientRootLoading() {
 | 
			
		||||
  return (
 | 
			
		||||
| 
						 | 
				
			
			@ -147,13 +148,14 @@ type ClientRootProps = {
 | 
			
		|||
export function ClientRoot({ children }: ClientRootProps) {
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
  const { baseUrl } = getSecret();
 | 
			
		||||
  const clientConfig = useClientConfig();
 | 
			
		||||
 | 
			
		||||
  const [loadState, loadMatrix] = useAsyncCallback<MatrixClient, Error, []>(
 | 
			
		||||
    useCallback(() => initClient(getSecret() as any), [])
 | 
			
		||||
    useCallback(() => initClient(getSecret() as any, clientConfig), [clientConfig])
 | 
			
		||||
  );
 | 
			
		||||
  const mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined;
 | 
			
		||||
  const [startState, startMatrix] = useAsyncCallback<void, Error, [MatrixClient]>(
 | 
			
		||||
    useCallback((m) => startClient(m), [])
 | 
			
		||||
    useCallback((m) => startClient(m, clientConfig), [clientConfig])
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useLogoutListener(mx);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
 | 
			
		|||
import { Box, config, Line, Text } from 'folds';
 | 
			
		||||
import { useSyncState } from '../../hooks/useSyncState';
 | 
			
		||||
import { ContainerColor } from '../../styles/ContainerColor.css';
 | 
			
		||||
import { useSlidingSyncEnabled, useSlidingSyncState, useSlidingSyncError } from '../../state/sliding-sync';
 | 
			
		||||
 | 
			
		||||
type StateData = {
 | 
			
		||||
  current: SyncState | null;
 | 
			
		||||
| 
						 | 
				
			
			@ -18,6 +19,10 @@ export function SyncStatus({ mx }: SyncStatusProps) {
 | 
			
		|||
    previous: undefined,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const slidingSyncEnabled = useSlidingSyncEnabled();
 | 
			
		||||
  const slidingSyncState = useSlidingSyncState();
 | 
			
		||||
  const slidingSyncError = useSlidingSyncError();
 | 
			
		||||
 | 
			
		||||
  useSyncState(
 | 
			
		||||
    mx,
 | 
			
		||||
    useCallback((current, previous) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +35,45 @@ export function SyncStatus({ mx }: SyncStatusProps) {
 | 
			
		|||
    }, [])
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // Handle sliding sync status if enabled
 | 
			
		||||
  if (slidingSyncEnabled) {
 | 
			
		||||
    if (slidingSyncError) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Box direction="Column" shrink="No">
 | 
			
		||||
          <Box
 | 
			
		||||
            className={ContainerColor({ variant: 'Critical' })}
 | 
			
		||||
            style={{ padding: `${config.space.S100} 0` }}
 | 
			
		||||
            alignItems="Center"
 | 
			
		||||
            justifyContent="Center"
 | 
			
		||||
          >
 | 
			
		||||
            <Text size="L400">Sliding Sync Error!</Text>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Line variant="Critical" size="300" />
 | 
			
		||||
        </Box>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (slidingSyncState === 'SYNCING') {
 | 
			
		||||
      return (
 | 
			
		||||
        <Box direction="Column" shrink="No">
 | 
			
		||||
          <Box
 | 
			
		||||
            className={ContainerColor({ variant: 'Primary' })}
 | 
			
		||||
            style={{ padding: `${config.space.S100} 0` }}
 | 
			
		||||
            alignItems="Center"
 | 
			
		||||
            justifyContent="Center"
 | 
			
		||||
          >
 | 
			
		||||
            <Text size="L400">Sliding Sync Active...</Text>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Line variant="Primary" size="300" />
 | 
			
		||||
        </Box>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Don't show traditional sync status when sliding sync is active
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Traditional sync status handling
 | 
			
		||||
  if (
 | 
			
		||||
    (stateData.current === SyncState.Prepared ||
 | 
			
		||||
      stateData.current === SyncState.Syncing ||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,13 +5,28 @@ import { mDirectAtom, useBindMDirectAtom } from '../mDirectList';
 | 
			
		|||
import { roomToUnreadAtom, useBindRoomToUnreadAtom } from '../room/roomToUnread';
 | 
			
		||||
import { roomToParentsAtom, useBindRoomToParentsAtom } from '../room/roomToParents';
 | 
			
		||||
import { roomIdToTypingMembersAtom, useBindRoomIdToTypingMembersAtom } from '../typingMembers';
 | 
			
		||||
import { useBindSlidingSyncAtom, useSlidingSyncRoomListBridge, useShouldUseSlidingSync } from '../sliding-sync';
 | 
			
		||||
import { getSlidingSync } from '../../../client/initMatrix';
 | 
			
		||||
 | 
			
		||||
export const useBindAtoms = (mx: MatrixClient) => {
 | 
			
		||||
  const slidingSync = getSlidingSync();
 | 
			
		||||
  const shouldUseSlidingSync = useShouldUseSlidingSync();
 | 
			
		||||
 | 
			
		||||
  // Bind sliding sync atoms if enabled
 | 
			
		||||
  useBindSlidingSyncAtom(mx, slidingSync);
 | 
			
		||||
  
 | 
			
		||||
  // Bridge sliding sync data to existing room list atoms
 | 
			
		||||
  useSlidingSyncRoomListBridge();
 | 
			
		||||
 | 
			
		||||
  // Only bind traditional room list atoms if not using sliding sync
 | 
			
		||||
  if (!shouldUseSlidingSync) {
 | 
			
		||||
    useBindAllInvitesAtom(mx, allInvitesAtom);
 | 
			
		||||
    useBindAllRoomsAtom(mx, allRoomsAtom);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // These atoms are always bound regardless of sync method
 | 
			
		||||
  useBindMDirectAtom(mx, mDirectAtom);
 | 
			
		||||
  useBindAllInvitesAtom(mx, allInvitesAtom);
 | 
			
		||||
  useBindAllRoomsAtom(mx, allRoomsAtom);
 | 
			
		||||
  useBindRoomToParentsAtom(mx, roomToParentsAtom);
 | 
			
		||||
  useBindRoomToUnreadAtom(mx, roomToUnreadAtom);
 | 
			
		||||
 | 
			
		||||
  useBindRoomIdToTypingMembersAtom(mx, roomIdToTypingMembersAtom);
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										3
									
								
								src/app/state/sliding-sync/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/app/state/sliding-sync/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
export * from './slidingSync';
 | 
			
		||||
export * from './useSlidingSync';
 | 
			
		||||
export * from './roomListBridge';
 | 
			
		||||
							
								
								
									
										40
									
								
								src/app/state/sliding-sync/roomListBridge.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/app/state/sliding-sync/roomListBridge.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
import { useSetAtom, useAtomValue } from 'jotai';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { allRoomsAtom } from '../room-list/roomList';
 | 
			
		||||
import { allInvitesAtom } from '../room-list/inviteList';
 | 
			
		||||
import { slidingSyncRoomListAtom, slidingSyncEnabledAtom } from './slidingSync';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Bridge sliding sync room list data to existing Cinny room list atoms
 | 
			
		||||
 * This allows existing UI components to work seamlessly with sliding sync
 | 
			
		||||
 */
 | 
			
		||||
export const useSlidingSyncRoomListBridge = () => {
 | 
			
		||||
  const slidingSyncEnabled = useAtomValue(slidingSyncEnabledAtom);
 | 
			
		||||
  const slidingSyncRoomList = useAtomValue(slidingSyncRoomListAtom);
 | 
			
		||||
  const setAllRooms = useSetAtom(allRoomsAtom);
 | 
			
		||||
  const setAllInvites = useSetAtom(allInvitesAtom);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!slidingSyncEnabled) return;
 | 
			
		||||
 | 
			
		||||
    // Bridge sliding sync room lists to existing atoms
 | 
			
		||||
    const allRoomsList = slidingSyncRoomList.allRooms || [];
 | 
			
		||||
    const directMessagesList = slidingSyncRoomList.directMessages || [];
 | 
			
		||||
    const invitesList = slidingSyncRoomList.invites || [];
 | 
			
		||||
 | 
			
		||||
    // Combine all rooms and DMs for the all rooms atom
 | 
			
		||||
    const combinedRooms = [...allRoomsList, ...directMessagesList];
 | 
			
		||||
 | 
			
		||||
    // Update existing atoms
 | 
			
		||||
    setAllRooms({ type: 'INITIALIZE', rooms: combinedRooms });
 | 
			
		||||
    setAllInvites({ type: 'INITIALIZE', invites: invitesList });
 | 
			
		||||
 | 
			
		||||
  }, [slidingSyncEnabled, slidingSyncRoomList, setAllRooms, setAllInvites]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hook to determine if sliding sync should override traditional sync behavior
 | 
			
		||||
 */
 | 
			
		||||
export const useShouldUseSlidingSync = () => {
 | 
			
		||||
  return useAtomValue(slidingSyncEnabledAtom);
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										20
									
								
								src/app/state/sliding-sync/slidingSync.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/app/state/sliding-sync/slidingSync.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
import { atom } from 'jotai';
 | 
			
		||||
import { SlidingSync } from 'matrix-js-sdk/lib/sliding-sync';
 | 
			
		||||
 | 
			
		||||
// Sliding sync instance atom
 | 
			
		||||
export const slidingSyncAtom = atom<SlidingSync | null>(null);
 | 
			
		||||
 | 
			
		||||
// Sliding sync state atom
 | 
			
		||||
export const slidingSyncStateAtom = atom<'PREPARED' | 'SYNCING' | 'STOPPED' | 'ERROR'>('STOPPED');
 | 
			
		||||
 | 
			
		||||
// Sliding sync enabled atom
 | 
			
		||||
export const slidingSyncEnabledAtom = atom<boolean>(false);
 | 
			
		||||
 | 
			
		||||
// Room list data from sliding sync
 | 
			
		||||
export const slidingSyncRoomListAtom = atom<Record<string, string[]>>({});
 | 
			
		||||
 | 
			
		||||
// Room data from sliding sync  
 | 
			
		||||
export const slidingSyncRoomDataAtom = atom<Record<string, any>>({});
 | 
			
		||||
 | 
			
		||||
// Error state for sliding sync
 | 
			
		||||
export const slidingSyncErrorAtom = atom<Error | null>(null);
 | 
			
		||||
							
								
								
									
										79
									
								
								src/app/state/sliding-sync/useSlidingSync.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/app/state/sliding-sync/useSlidingSync.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,79 @@
 | 
			
		|||
import { useSetAtom, useAtomValue } from 'jotai';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { MatrixClient } from 'matrix-js-sdk';
 | 
			
		||||
import { SlidingSync, SlidingSyncEvent } from 'matrix-js-sdk/lib/sliding-sync';
 | 
			
		||||
import { 
 | 
			
		||||
  slidingSyncAtom, 
 | 
			
		||||
  slidingSyncStateAtom, 
 | 
			
		||||
  slidingSyncEnabledAtom,
 | 
			
		||||
  slidingSyncRoomListAtom,
 | 
			
		||||
  slidingSyncRoomDataAtom,
 | 
			
		||||
  slidingSyncErrorAtom
 | 
			
		||||
} from './slidingSync';
 | 
			
		||||
 | 
			
		||||
export const useBindSlidingSyncAtom = (mx: MatrixClient, slidingSync: SlidingSync | null) => {
 | 
			
		||||
  const setSlidingSync = useSetAtom(slidingSyncAtom);
 | 
			
		||||
  const setSlidingSyncState = useSetAtom(slidingSyncStateAtom);
 | 
			
		||||
  const setSlidingSyncEnabled = useSetAtom(slidingSyncEnabledAtom);
 | 
			
		||||
  const setSlidingSyncRoomList = useSetAtom(slidingSyncRoomListAtom);
 | 
			
		||||
  const setSlidingSyncRoomData = useSetAtom(slidingSyncRoomDataAtom);
 | 
			
		||||
  const setSlidingSyncError = useSetAtom(slidingSyncErrorAtom);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!slidingSync) {
 | 
			
		||||
      setSlidingSyncEnabled(false);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setSlidingSync(slidingSync);
 | 
			
		||||
    setSlidingSyncEnabled(true);
 | 
			
		||||
 | 
			
		||||
    const handleSyncUpdate = () => {
 | 
			
		||||
      setSlidingSyncState('SYNCING');
 | 
			
		||||
      
 | 
			
		||||
      // Extract room lists
 | 
			
		||||
      const lists: Record<string, string[]> = {};
 | 
			
		||||
      slidingSync.getListData().forEach((listData, listKey) => {
 | 
			
		||||
        lists[listKey] = listData.joinedRooms || [];
 | 
			
		||||
      });
 | 
			
		||||
      setSlidingSyncRoomList(lists);
 | 
			
		||||
 | 
			
		||||
      // Extract room data
 | 
			
		||||
      const roomData: Record<string, any> = {};
 | 
			
		||||
      slidingSync.getRoomData().forEach((data, roomId) => {
 | 
			
		||||
        roomData[roomId] = data;
 | 
			
		||||
      });
 | 
			
		||||
      setSlidingSyncRoomData(roomData);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleSyncComplete = () => {
 | 
			
		||||
      setSlidingSyncState('PREPARED');
 | 
			
		||||
      setSlidingSyncError(null);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleSyncError = (error: Error) => {
 | 
			
		||||
      setSlidingSyncState('ERROR');
 | 
			
		||||
      setSlidingSyncError(error);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Bind sliding sync events
 | 
			
		||||
    slidingSync.on(SlidingSyncEvent.List, handleSyncUpdate);
 | 
			
		||||
    slidingSync.on(SlidingSyncEvent.RoomData, handleSyncUpdate);
 | 
			
		||||
    // Note: These event names might need adjustment based on actual SDK events
 | 
			
		||||
    slidingSync.on('sync' as any, handleSyncComplete);
 | 
			
		||||
    slidingSync.on('error' as any, handleSyncError);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      slidingSync.removeListener(SlidingSyncEvent.List, handleSyncUpdate);
 | 
			
		||||
      slidingSync.removeListener(SlidingSyncEvent.RoomData, handleSyncUpdate);
 | 
			
		||||
      slidingSync.removeListener('sync' as any, handleSyncComplete);
 | 
			
		||||
      slidingSync.removeListener('error' as any, handleSyncError);
 | 
			
		||||
    };
 | 
			
		||||
  }, [mx, slidingSync, setSlidingSync, setSlidingSyncState, setSlidingSyncEnabled, setSlidingSyncRoomList, setSlidingSyncRoomData, setSlidingSyncError]);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useSlidingSyncEnabled = () => useAtomValue(slidingSyncEnabledAtom);
 | 
			
		||||
export const useSlidingSyncState = () => useAtomValue(slidingSyncStateAtom);
 | 
			
		||||
export const useSlidingSyncRoomList = () => useAtomValue(slidingSyncRoomListAtom);
 | 
			
		||||
export const useSlidingSyncRoomData = () => useAtomValue(slidingSyncRoomDataAtom);
 | 
			
		||||
export const useSlidingSyncError = () => useAtomValue(slidingSyncErrorAtom);
 | 
			
		||||
							
								
								
									
										65
									
								
								src/app/styles/PurpleGradient.css.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/app/styles/PurpleGradient.css.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,65 @@
 | 
			
		|||
import { style } from '@vanilla-extract/css';
 | 
			
		||||
import { color } from 'folds';
 | 
			
		||||
 | 
			
		||||
export const purpleGradientButton = style({
 | 
			
		||||
  background: 'linear-gradient(135deg, #6B46FF, #9D5EFF)',
 | 
			
		||||
  border: 'none',
 | 
			
		||||
  color: '#FFFFFF',
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  transition: 'all 0.3s ease',
 | 
			
		||||
  boxShadow: '0 4px 15px rgba(107, 70, 255, 0.2)',
 | 
			
		||||
  
 | 
			
		||||
  '::before': {
 | 
			
		||||
    content: '""',
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    top: 0,
 | 
			
		||||
    left: '-100%',
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '100%',
 | 
			
		||||
    background: 'linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent)',
 | 
			
		||||
    transition: 'left 0.5s ease',
 | 
			
		||||
  },
 | 
			
		||||
  
 | 
			
		||||
  ':hover': {
 | 
			
		||||
    background: 'linear-gradient(135deg, #5A3AE5, #8B4FE6)',
 | 
			
		||||
    transform: 'translateY(-2px)',
 | 
			
		||||
    boxShadow: '0 10px 25px rgba(107, 70, 255, 0.4)',
 | 
			
		||||
  },
 | 
			
		||||
  
 | 
			
		||||
  ':hover::before': {
 | 
			
		||||
    left: '100%',
 | 
			
		||||
  },
 | 
			
		||||
  
 | 
			
		||||
  ':active': {
 | 
			
		||||
    transform: 'translateY(0)',
 | 
			
		||||
    background: 'linear-gradient(135deg, #4A2FD4, #7A40CC)',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const purpleGradientCard = style({
 | 
			
		||||
  background: 'linear-gradient(135deg, rgba(107, 70, 255, 0.1), rgba(157, 94, 255, 0.05))',
 | 
			
		||||
  border: '1px solid rgba(107, 70, 255, 0.2)',
 | 
			
		||||
  backdropFilter: 'blur(10px)',
 | 
			
		||||
  
 | 
			
		||||
  ':hover': {
 | 
			
		||||
    background: 'linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08))',
 | 
			
		||||
    border: '1px solid rgba(107, 70, 255, 0.3)',
 | 
			
		||||
    transform: 'translateY(-2px)',
 | 
			
		||||
    boxShadow: '0 10px 30px rgba(107, 70, 255, 0.2)',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const purpleThemeColors = style({
 | 
			
		||||
  selectors: {
 | 
			
		||||
    '.purple-theme &': {
 | 
			
		||||
      '--accent-gradient': 'linear-gradient(135deg, #6B46FF, #9D5EFF)',
 | 
			
		||||
      '--accent-gradient-hover': 'linear-gradient(135deg, #5A3AE5, #8B4FE6)',
 | 
			
		||||
      '--accent-gradient-active': 'linear-gradient(135deg, #4A2FD4, #7A40CC)',
 | 
			
		||||
      '--surface-gradient': 'linear-gradient(135deg, rgba(107, 70, 255, 0.1), rgba(157, 94, 255, 0.05))',
 | 
			
		||||
      '--surface-gradient-hover': 'linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08))',
 | 
			
		||||
      '--purple-glow': '0 10px 30px rgba(107, 70, 255, 0.2)',
 | 
			
		||||
      '--purple-glow-strong': '0 10px 30px rgba(107, 70, 255, 0.4)',
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										76
									
								
								src/app/utils/sliding-sync.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/app/utils/sliding-sync.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,76 @@
 | 
			
		|||
/**
 | 
			
		||||
 * Validates a sliding sync proxy URL
 | 
			
		||||
 */
 | 
			
		||||
export const validateSlidingSyncProxyUrl = (url: string): boolean => {
 | 
			
		||||
  try {
 | 
			
		||||
    const parsed = new URL(url);
 | 
			
		||||
    // Must be HTTPS for security
 | 
			
		||||
    if (parsed.protocol !== 'https:') return false;
 | 
			
		||||
    // Must have a hostname
 | 
			
		||||
    if (!parsed.hostname) return false;
 | 
			
		||||
    return true;
 | 
			
		||||
  } catch {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets a human-readable error message for invalid proxy URLs
 | 
			
		||||
 */
 | 
			
		||||
export const getSlidingSyncProxyUrlError = (url: string): string | null => {
 | 
			
		||||
  if (!url) return 'Proxy URL is required';
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    const parsed = new URL(url);
 | 
			
		||||
    if (parsed.protocol !== 'https:') {
 | 
			
		||||
      return 'Proxy URL must use HTTPS for security';
 | 
			
		||||
    }
 | 
			
		||||
    if (!parsed.hostname) {
 | 
			
		||||
      return 'Proxy URL must have a valid hostname';
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  } catch {
 | 
			
		||||
    return 'Invalid URL format';
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Common sliding sync proxy URLs for reference
 | 
			
		||||
 */
 | 
			
		||||
export const SLIDING_SYNC_PROXY_EXAMPLES = [
 | 
			
		||||
  'https://syncv3.matrix.org',
 | 
			
		||||
  'https://sliding-sync.matrix.org',
 | 
			
		||||
  'https://your-server.com/_matrix/sliding-sync',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default sliding sync list configurations
 | 
			
		||||
 */
 | 
			
		||||
export const DEFAULT_SLIDING_SYNC_LISTS = {
 | 
			
		||||
  allRooms: {
 | 
			
		||||
    ranges: [[0, 49]],
 | 
			
		||||
    sort: ['by_recency', 'by_name'],
 | 
			
		||||
    timeline_limit: 1,
 | 
			
		||||
    required_state: [
 | 
			
		||||
      ['m.room.name', ''],
 | 
			
		||||
      ['m.room.avatar', ''],
 | 
			
		||||
      ['m.room.canonical_alias', ''],
 | 
			
		||||
      ['m.room.topic', ''],
 | 
			
		||||
      ['m.room.encryption', ''],
 | 
			
		||||
      ['m.room.member', '$ME'],
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  directMessages: {
 | 
			
		||||
    ranges: [[0, 49]],
 | 
			
		||||
    sort: ['by_recency'],
 | 
			
		||||
    timeline_limit: 1,
 | 
			
		||||
    filters: {
 | 
			
		||||
      is_dm: true,
 | 
			
		||||
    },
 | 
			
		||||
    required_state: [
 | 
			
		||||
      ['m.room.name', ''],
 | 
			
		||||
      ['m.room.avatar', ''],
 | 
			
		||||
      ['m.room.member', '$ME'],
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,10 @@
 | 
			
		|||
import { createClient, MatrixClient, IndexedDBStore, IndexedDBCryptoStore } from 'matrix-js-sdk';
 | 
			
		||||
import { SlidingSync } from 'matrix-js-sdk/lib/sliding-sync';
 | 
			
		||||
import { SlidingSyncSdk } from 'matrix-js-sdk/lib/sliding-sync-sdk';
 | 
			
		||||
 | 
			
		||||
import { cryptoCallbacks } from './state/secretStorageKeys';
 | 
			
		||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
 | 
			
		||||
import { ClientConfig } from '../app/hooks/useClientConfig';
 | 
			
		||||
 | 
			
		||||
type Session = {
 | 
			
		||||
  baseUrl: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +13,7 @@ type Session = {
 | 
			
		|||
  deviceId: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const initClient = async (session: Session): Promise<MatrixClient> => {
 | 
			
		||||
export const initClient = async (session: Session, clientConfig?: ClientConfig): Promise<MatrixClient> => {
 | 
			
		||||
  const indexedDBStore = new IndexedDBStore({
 | 
			
		||||
    indexedDB: global.indexedDB,
 | 
			
		||||
    localStorage: global.localStorage,
 | 
			
		||||
| 
						 | 
				
			
			@ -39,7 +42,63 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
 | 
			
		|||
  return mx;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const startClient = async (mx: MatrixClient) => {
 | 
			
		||||
// Store sliding sync instance globally for atom access
 | 
			
		||||
let globalSlidingSync: SlidingSync | null = null;
 | 
			
		||||
 | 
			
		||||
export const getSlidingSync = (): SlidingSync | null => globalSlidingSync;
 | 
			
		||||
 | 
			
		||||
export const startClient = async (mx: MatrixClient, clientConfig?: ClientConfig) => {
 | 
			
		||||
  // Check if sliding sync is enabled
 | 
			
		||||
  if (clientConfig?.slidingSync?.enabled) {
 | 
			
		||||
    try {
 | 
			
		||||
      const lists = new Map();
 | 
			
		||||
      Object.entries(clientConfig.slidingSync.defaultLists || {}).forEach(([key, config]) => {
 | 
			
		||||
        lists.set(key, {
 | 
			
		||||
          ranges: config.ranges,
 | 
			
		||||
          sort: config.sort || ['by_recency'],
 | 
			
		||||
          timeline_limit: config.timeline_limit || 1,
 | 
			
		||||
          required_state: config.required_state || [],
 | 
			
		||||
          filters: config.filters,
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Use native sliding sync if no proxy URL, otherwise use proxy
 | 
			
		||||
      const proxyUrl = clientConfig.slidingSync.proxyUrl;
 | 
			
		||||
      const slidingSync = new SlidingSync(
 | 
			
		||||
        proxyUrl || mx.baseUrl, // Use homeserver base URL for native sliding sync
 | 
			
		||||
        lists,
 | 
			
		||||
        {},
 | 
			
		||||
        mx,
 | 
			
		||||
        5000 // timeout
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Store globally for atom access
 | 
			
		||||
      globalSlidingSync = slidingSync;
 | 
			
		||||
 | 
			
		||||
      // Use SlidingSyncSdk as a drop-in replacement for traditional sync
 | 
			
		||||
      const slidingSyncSdk = new SlidingSyncSdk(slidingSync, mx);
 | 
			
		||||
      
 | 
			
		||||
      // Start sliding sync with timeout and error handling
 | 
			
		||||
      const slidingSyncPromise = slidingSync.start();
 | 
			
		||||
      const timeoutPromise = new Promise((_, reject) => 
 | 
			
		||||
        setTimeout(() => reject(new Error('Sliding sync timeout')), 10000)
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      await Promise.race([slidingSyncPromise, timeoutPromise]);
 | 
			
		||||
      
 | 
			
		||||
      console.log('Sliding sync started successfully');
 | 
			
		||||
      return;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.warn('Sliding sync failed, falling back to traditional sync:', error);
 | 
			
		||||
      globalSlidingSync = null;
 | 
			
		||||
      // Fall through to traditional sync
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Reset global sliding sync for traditional sync
 | 
			
		||||
  globalSlidingSync = null;
 | 
			
		||||
 | 
			
		||||
  // Fallback to traditional sync
 | 
			
		||||
  await mx.startClient({
 | 
			
		||||
    lazyLoadMembers: true,
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -236,3 +236,100 @@ export const butterTheme = createTheme(color, {
 | 
			
		|||
    OnContainer: '#F2EED3',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const purpleTheme = createTheme(color, {
 | 
			
		||||
  Background: {
 | 
			
		||||
    Container: '#1A1B23',
 | 
			
		||||
    ContainerHover: '#2A2B35',
 | 
			
		||||
    ContainerActive: '#3A3B47',
 | 
			
		||||
    ContainerLine: '#4A4B57',
 | 
			
		||||
    OnContainer: '#FFFFFF',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  Surface: {
 | 
			
		||||
    Container: '#2A2B35',
 | 
			
		||||
    ContainerHover: '#3A3B47',
 | 
			
		||||
    ContainerActive: '#4A4B57',
 | 
			
		||||
    ContainerLine: '#5A5B67',
 | 
			
		||||
    OnContainer: '#FFFFFF',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  SurfaceVariant: {
 | 
			
		||||
    Container: '#3A3B47',
 | 
			
		||||
    ContainerHover: '#4A4B57',
 | 
			
		||||
    ContainerActive: '#5A5B67',
 | 
			
		||||
    ContainerLine: '#6A6B77',
 | 
			
		||||
    OnContainer: '#FFFFFF',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  Primary: {
 | 
			
		||||
    Main: '#6B46FF',
 | 
			
		||||
    MainHover: '#5A3AE5',
 | 
			
		||||
    MainActive: '#4A2FD4',
 | 
			
		||||
    MainLine: '#3A1FBF',
 | 
			
		||||
    OnMain: '#FFFFFF',
 | 
			
		||||
    Container: '#F4F1FF',
 | 
			
		||||
    ContainerHover: '#E9E4FF',
 | 
			
		||||
    ContainerActive: '#DDD6FF',
 | 
			
		||||
    ContainerLine: '#D1C8FF',
 | 
			
		||||
    OnContainer: '#2D1B7B',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  Secondary: {
 | 
			
		||||
    Main: '#9D5EFF',
 | 
			
		||||
    MainHover: '#8B4FE6',
 | 
			
		||||
    MainActive: '#7A40CC',
 | 
			
		||||
    MainLine: '#6930B3',
 | 
			
		||||
    OnMain: '#FFFFFF',
 | 
			
		||||
    Container: '#B89FFF',
 | 
			
		||||
    ContainerHover: '#B299FF',
 | 
			
		||||
    ContainerActive: '#AC93FF',
 | 
			
		||||
    ContainerLine: '#A68DFF',
 | 
			
		||||
    OnContainer: '#2D1B7B',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  Success: {
 | 
			
		||||
    Main: '#10B981',
 | 
			
		||||
    MainHover: '#059669',
 | 
			
		||||
    MainActive: '#047857',
 | 
			
		||||
    MainLine: '#065F46',
 | 
			
		||||
    OnMain: '#FFFFFF',
 | 
			
		||||
    Container: '#D1FAE5',
 | 
			
		||||
    ContainerHover: '#A7F3D0',
 | 
			
		||||
    ContainerActive: '#6EE7B7',
 | 
			
		||||
    ContainerLine: '#34D399',
 | 
			
		||||
    OnContainer: '#065F46',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  Warning: {
 | 
			
		||||
    Main: '#F59E0B',
 | 
			
		||||
    MainHover: '#D97706',
 | 
			
		||||
    MainActive: '#B45309',
 | 
			
		||||
    MainLine: '#92400E',
 | 
			
		||||
    OnMain: '#FFFFFF',
 | 
			
		||||
    Container: '#FEF3C7',
 | 
			
		||||
    ContainerHover: '#FDE68A',
 | 
			
		||||
    ContainerActive: '#FCD34D',
 | 
			
		||||
    ContainerLine: '#FBBF24',
 | 
			
		||||
    OnContainer: '#92400E',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  Critical: {
 | 
			
		||||
    Main: '#EF4444',
 | 
			
		||||
    MainHover: '#DC2626',
 | 
			
		||||
    MainActive: '#B91C1C',
 | 
			
		||||
    MainLine: '#991B1B',
 | 
			
		||||
    OnMain: '#FFFFFF',
 | 
			
		||||
    Container: '#FEE2E2',
 | 
			
		||||
    ContainerHover: '#FECACA',
 | 
			
		||||
    ContainerActive: '#FCA5A5',
 | 
			
		||||
    ContainerLine: '#F87171',
 | 
			
		||||
    OnContainer: '#991B1B',
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  Other: {
 | 
			
		||||
    FocusRing: 'rgba(107, 70, 255, 0.5)',
 | 
			
		||||
    Shadow: 'rgba(0, 0, 0, 1)',
 | 
			
		||||
    Overlay: 'rgba(0, 0, 0, 0.8)',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,73 @@
 | 
			
		|||
  font-display: swap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Purple theme gradient enhancements
 | 
			
		||||
.purple-theme {
 | 
			
		||||
  // Gradient buttons for folds components
 | 
			
		||||
  .Button[data-variant='Primary'] {
 | 
			
		||||
    background: linear-gradient(135deg, #6B46FF, #9D5EFF) !important;
 | 
			
		||||
    box-shadow: 0 4px 15px rgba(107, 70, 255, 0.2);
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    transition: all 0.3s ease;
 | 
			
		||||
    
 | 
			
		||||
    &::before {
 | 
			
		||||
      content: '';
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      left: -100%;
 | 
			
		||||
      width: 100%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
 | 
			
		||||
      transition: left 0.5s ease;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background: linear-gradient(135deg, #5A3AE5, #8B4FE6) !important;
 | 
			
		||||
      transform: translateY(-2px);
 | 
			
		||||
      box-shadow: 0 10px 25px rgba(107, 70, 255, 0.4);
 | 
			
		||||
      
 | 
			
		||||
      &::before {
 | 
			
		||||
        left: 100%;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    &:active {
 | 
			
		||||
      transform: translateY(0);
 | 
			
		||||
      background: linear-gradient(135deg, #4A2FD4, #7A40CC) !important;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Enhanced cards and surfaces with subtle gradients
 | 
			
		||||
  .SequenceCard,
 | 
			
		||||
  .InfoCard,
 | 
			
		||||
  .RoomCard {
 | 
			
		||||
    background: linear-gradient(135deg, rgba(107, 70, 255, 0.05), rgba(157, 94, 255, 0.02));
 | 
			
		||||
    border: 1px solid rgba(107, 70, 255, 0.1);
 | 
			
		||||
    
 | 
			
		||||
    &:hover {
 | 
			
		||||
      background: linear-gradient(135deg, rgba(107, 70, 255, 0.1), rgba(157, 94, 255, 0.05));
 | 
			
		||||
      border: 1px solid rgba(107, 70, 255, 0.2);
 | 
			
		||||
      transform: translateY(-1px);
 | 
			
		||||
      box-shadow: 0 8px 25px rgba(107, 70, 255, 0.15);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Navigation items with purple accents
 | 
			
		||||
  .NavItem[data-selected='true'] {
 | 
			
		||||
    background: linear-gradient(135deg, rgba(107, 70, 255, 0.15), rgba(157, 94, 255, 0.08)) !important;
 | 
			
		||||
    border-left: 3px solid #6B46FF;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Room input with gradient border focus
 | 
			
		||||
  .RoomInput {
 | 
			
		||||
    &:focus-within {
 | 
			
		||||
      border: 1px solid #6B46FF;
 | 
			
		||||
      box-shadow: 0 0 0 3px rgba(107, 70, 255, 0.1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  /* background color | --bg-[background type]: value */
 | 
			
		||||
  --bg-surface: #ffffff;
 | 
			
		||||
| 
						 | 
				
			
			@ -193,8 +260,8 @@
 | 
			
		|||
  --fluid-slide-up: cubic-bezier(0.13, 0.56, 0.25, 0.99);
 | 
			
		||||
 | 
			
		||||
  --font-emoji: 'Twemoji_DISABLED';
 | 
			
		||||
  --font-primary: 'InterVariable', var(--font-emoji), sans-serif;
 | 
			
		||||
  --font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
 | 
			
		||||
  --font-primary: 'SF Pro Display', var(--font-emoji), sans-serif;
 | 
			
		||||
  --font-secondary: 'SF Pro Display', var(--font-emoji), sans-serif;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.silver-theme {
 | 
			
		||||
| 
						 | 
				
			
			@ -326,7 +393,7 @@
 | 
			
		|||
  --fw-medium: 450;
 | 
			
		||||
  --fw-bold: 550;
 | 
			
		||||
 | 
			
		||||
  --font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
 | 
			
		||||
  --font-secondary: 'SF Pro Display', var(--font-emoji), sans-serif;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.butter-theme {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue