mirror of
https://github.com/cinnyapp/cinny.git
synced 2025-11-05 15:00:30 +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": {
|
"hashRouter": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"basename": "/"
|
"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}/`);
|
||||||
|
});
|
||||||
|
|
@ -79,3 +79,38 @@
|
||||||
@include state.focus(var(--bs-danger-outline));
|
@include state.focus(var(--bs-danger-outline));
|
||||||
@include state.active(var(--bg-danger-active));
|
@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 { Devices } from './devices';
|
||||||
import { EmojisStickers } from './emojis-stickers';
|
import { EmojisStickers } from './emojis-stickers';
|
||||||
import { DeveloperTools } from './developer-tools';
|
import { DeveloperTools } from './developer-tools';
|
||||||
|
// Temporarily disabled for debugging
|
||||||
|
// import { SlidingSyncSettings } from './sliding-sync';
|
||||||
import { About } from './about';
|
import { About } from './about';
|
||||||
import { UseStateProvider } from '../../components/UseStateProvider';
|
import { UseStateProvider } from '../../components/UseStateProvider';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
@ -40,6 +42,7 @@ export enum SettingsPages {
|
||||||
NotificationPage,
|
NotificationPage,
|
||||||
DevicesPage,
|
DevicesPage,
|
||||||
EmojisStickersPage,
|
EmojisStickersPage,
|
||||||
|
SlidingSyncPage,
|
||||||
DeveloperToolsPage,
|
DeveloperToolsPage,
|
||||||
AboutPage,
|
AboutPage,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ import { stopPropagation } from '../../../utils/keyboard';
|
||||||
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
|
import { useMessageLayoutItems } from '../../../hooks/useMessageLayout';
|
||||||
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
|
import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing';
|
||||||
import { SequenceCardStyle } from '../styles.css';
|
import { SequenceCardStyle } from '../styles.css';
|
||||||
|
import { useClientConfig } from '../../../hooks/useClientConfig';
|
||||||
|
|
||||||
type ThemeSelectorProps = {
|
type ThemeSelectorProps = {
|
||||||
themeNames: Record<string, string>;
|
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 = {
|
type GeneralProps = {
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
};
|
};
|
||||||
|
|
@ -639,6 +684,7 @@ export function General({ requestClose }: GeneralProps) {
|
||||||
<Appearance />
|
<Appearance />
|
||||||
<Editor />
|
<Editor />
|
||||||
<Messages />
|
<Messages />
|
||||||
|
<Synchronization />
|
||||||
</Box>
|
</Box>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Scroll>
|
</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;
|
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 = {
|
export type ClientConfig = {
|
||||||
defaultHomeserver?: number;
|
defaultHomeserver?: number;
|
||||||
homeserverList?: string[];
|
homeserverList?: string[];
|
||||||
|
|
@ -18,6 +32,7 @@ export type ClientConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
hashRouter?: HashRouterConfig;
|
hashRouter?: HashRouterConfig;
|
||||||
|
slidingSync?: SlidingSyncConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ClientConfigContext = createContext<ClientConfig | null>(null);
|
const ClientConfigContext = createContext<ClientConfig | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { lightTheme } from 'folds';
|
import { lightTheme } from 'folds';
|
||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
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 { settingsAtom } from '../state/settings';
|
||||||
import { useSetting } from '../state/hooks/settings';
|
import { useSetting } from '../state/hooks/settings';
|
||||||
|
|
||||||
|
|
@ -37,9 +37,14 @@ export const ButterTheme: Theme = {
|
||||||
kind: ThemeKind.Dark,
|
kind: ThemeKind.Dark,
|
||||||
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-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[] => {
|
export const useThemes = (): Theme[] => {
|
||||||
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
|
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme, PurpleTheme], []);
|
||||||
|
|
||||||
return themes;
|
return themes;
|
||||||
};
|
};
|
||||||
|
|
@ -51,6 +56,7 @@ export const useThemeNames = (): Record<string, string> =>
|
||||||
[SilverTheme.id]: 'Silver',
|
[SilverTheme.id]: 'Silver',
|
||||||
[DarkTheme.id]: 'Dark',
|
[DarkTheme.id]: 'Dark',
|
||||||
[ButterTheme.id]: 'Butter',
|
[ButterTheme.id]: 'Butter',
|
||||||
|
[PurpleTheme.id]: 'Purple',
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
@ -84,14 +90,14 @@ export const useActiveTheme = (): Theme => {
|
||||||
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||||
|
|
||||||
if (!systemTheme) {
|
if (!systemTheme) {
|
||||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? PurpleTheme;
|
||||||
|
|
||||||
return selectedTheme;
|
return selectedTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTheme =
|
const selectedTheme =
|
||||||
systemThemeKind === ThemeKind.Dark
|
systemThemeKind === ThemeKind.Dark
|
||||||
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
|
? themes.find((theme) => theme.id === darkThemeId) ?? PurpleTheme
|
||||||
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
||||||
|
|
||||||
return selectedTheme;
|
return selectedTheme;
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import { useSyncState } from '../../hooks/useSyncState';
|
||||||
import { stopPropagation } from '../../utils/keyboard';
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
import { SyncStatus } from './SyncStatus';
|
import { SyncStatus } from './SyncStatus';
|
||||||
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
|
import { AuthMetadataProvider } from '../../hooks/useAuthMetadata';
|
||||||
|
import { useClientConfig } from '../../hooks/useClientConfig';
|
||||||
|
|
||||||
function ClientRootLoading() {
|
function ClientRootLoading() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -147,13 +148,14 @@ type ClientRootProps = {
|
||||||
export function ClientRoot({ children }: ClientRootProps) {
|
export function ClientRoot({ children }: ClientRootProps) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const { baseUrl } = getSecret();
|
const { baseUrl } = getSecret();
|
||||||
|
const clientConfig = useClientConfig();
|
||||||
|
|
||||||
const [loadState, loadMatrix] = useAsyncCallback<MatrixClient, Error, []>(
|
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 mx = loadState.status === AsyncStatus.Success ? loadState.data : undefined;
|
||||||
const [startState, startMatrix] = useAsyncCallback<void, Error, [MatrixClient]>(
|
const [startState, startMatrix] = useAsyncCallback<void, Error, [MatrixClient]>(
|
||||||
useCallback((m) => startClient(m), [])
|
useCallback((m) => startClient(m, clientConfig), [clientConfig])
|
||||||
);
|
);
|
||||||
|
|
||||||
useLogoutListener(mx);
|
useLogoutListener(mx);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import React, { useCallback, useState } from 'react';
|
||||||
import { Box, config, Line, Text } from 'folds';
|
import { Box, config, Line, Text } from 'folds';
|
||||||
import { useSyncState } from '../../hooks/useSyncState';
|
import { useSyncState } from '../../hooks/useSyncState';
|
||||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||||
|
import { useSlidingSyncEnabled, useSlidingSyncState, useSlidingSyncError } from '../../state/sliding-sync';
|
||||||
|
|
||||||
type StateData = {
|
type StateData = {
|
||||||
current: SyncState | null;
|
current: SyncState | null;
|
||||||
|
|
@ -18,6 +19,10 @@ export function SyncStatus({ mx }: SyncStatusProps) {
|
||||||
previous: undefined,
|
previous: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const slidingSyncEnabled = useSlidingSyncEnabled();
|
||||||
|
const slidingSyncState = useSlidingSyncState();
|
||||||
|
const slidingSyncError = useSlidingSyncError();
|
||||||
|
|
||||||
useSyncState(
|
useSyncState(
|
||||||
mx,
|
mx,
|
||||||
useCallback((current, previous) => {
|
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 (
|
if (
|
||||||
(stateData.current === SyncState.Prepared ||
|
(stateData.current === SyncState.Prepared ||
|
||||||
stateData.current === SyncState.Syncing ||
|
stateData.current === SyncState.Syncing ||
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,28 @@ import { mDirectAtom, useBindMDirectAtom } from '../mDirectList';
|
||||||
import { roomToUnreadAtom, useBindRoomToUnreadAtom } from '../room/roomToUnread';
|
import { roomToUnreadAtom, useBindRoomToUnreadAtom } from '../room/roomToUnread';
|
||||||
import { roomToParentsAtom, useBindRoomToParentsAtom } from '../room/roomToParents';
|
import { roomToParentsAtom, useBindRoomToParentsAtom } from '../room/roomToParents';
|
||||||
import { roomIdToTypingMembersAtom, useBindRoomIdToTypingMembersAtom } from '../typingMembers';
|
import { roomIdToTypingMembersAtom, useBindRoomIdToTypingMembersAtom } from '../typingMembers';
|
||||||
|
import { useBindSlidingSyncAtom, useSlidingSyncRoomListBridge, useShouldUseSlidingSync } from '../sliding-sync';
|
||||||
|
import { getSlidingSync } from '../../../client/initMatrix';
|
||||||
|
|
||||||
export const useBindAtoms = (mx: MatrixClient) => {
|
export const useBindAtoms = (mx: MatrixClient) => {
|
||||||
useBindMDirectAtom(mx, mDirectAtom);
|
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);
|
useBindAllInvitesAtom(mx, allInvitesAtom);
|
||||||
useBindAllRoomsAtom(mx, allRoomsAtom);
|
useBindAllRoomsAtom(mx, allRoomsAtom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// These atoms are always bound regardless of sync method
|
||||||
|
useBindMDirectAtom(mx, mDirectAtom);
|
||||||
useBindRoomToParentsAtom(mx, roomToParentsAtom);
|
useBindRoomToParentsAtom(mx, roomToParentsAtom);
|
||||||
useBindRoomToUnreadAtom(mx, roomToUnreadAtom);
|
useBindRoomToUnreadAtom(mx, roomToUnreadAtom);
|
||||||
|
|
||||||
useBindRoomIdToTypingMembersAtom(mx, roomIdToTypingMembersAtom);
|
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 { 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 { cryptoCallbacks } from './state/secretStorageKeys';
|
||||||
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
import { clearNavToActivePathStore } from '../app/state/navToActivePath';
|
||||||
|
import { ClientConfig } from '../app/hooks/useClientConfig';
|
||||||
|
|
||||||
type Session = {
|
type Session = {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|
@ -10,7 +13,7 @@ type Session = {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initClient = async (session: Session): Promise<MatrixClient> => {
|
export const initClient = async (session: Session, clientConfig?: ClientConfig): Promise<MatrixClient> => {
|
||||||
const indexedDBStore = new IndexedDBStore({
|
const indexedDBStore = new IndexedDBStore({
|
||||||
indexedDB: global.indexedDB,
|
indexedDB: global.indexedDB,
|
||||||
localStorage: global.localStorage,
|
localStorage: global.localStorage,
|
||||||
|
|
@ -39,7 +42,63 @@ export const initClient = async (session: Session): Promise<MatrixClient> => {
|
||||||
return mx;
|
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({
|
await mx.startClient({
|
||||||
lazyLoadMembers: true,
|
lazyLoadMembers: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -236,3 +236,100 @@ export const butterTheme = createTheme(color, {
|
||||||
OnContainer: '#F2EED3',
|
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;
|
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 {
|
:root {
|
||||||
/* background color | --bg-[background type]: value */
|
/* background color | --bg-[background type]: value */
|
||||||
--bg-surface: #ffffff;
|
--bg-surface: #ffffff;
|
||||||
|
|
@ -193,8 +260,8 @@
|
||||||
--fluid-slide-up: cubic-bezier(0.13, 0.56, 0.25, 0.99);
|
--fluid-slide-up: cubic-bezier(0.13, 0.56, 0.25, 0.99);
|
||||||
|
|
||||||
--font-emoji: 'Twemoji_DISABLED';
|
--font-emoji: 'Twemoji_DISABLED';
|
||||||
--font-primary: 'InterVariable', var(--font-emoji), sans-serif;
|
--font-primary: 'SF Pro Display', var(--font-emoji), sans-serif;
|
||||||
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
|
--font-secondary: 'SF Pro Display', var(--font-emoji), sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.silver-theme {
|
.silver-theme {
|
||||||
|
|
@ -326,7 +393,7 @@
|
||||||
--fw-medium: 450;
|
--fw-medium: 450;
|
||||||
--fw-bold: 550;
|
--fw-bold: 550;
|
||||||
|
|
||||||
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
|
--font-secondary: 'SF Pro Display', var(--font-emoji), sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.butter-theme {
|
.butter-theme {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue