3622 lines
197 KiB
HTML
3622 lines
197 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="ru">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<meta http-equiv="Content-Security-Policy"
|
|||
|
|
content="default-src 'self';
|
|||
|
|
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com https://cdn.tailwindcss.com https://cdnjs.cloudflare.com https://static.cloudflareinsights.com;
|
|||
|
|
style-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com https://cdnjs.cloudflare.com https://fonts.googleapis.com;
|
|||
|
|
font-src 'self' https://cdnjs.cloudflare.com https://fonts.gstatic.com;
|
|||
|
|
connect-src 'self' https: https://cloudflareinsights.com;
|
|||
|
|
img-src 'self' data: https://api.qrserver.com;
|
|||
|
|
media-src 'none';
|
|||
|
|
object-src 'none';
|
|||
|
|
frame-src 'none';">
|
|||
|
|
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
|||
|
|
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
|
|||
|
|
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
|
|||
|
|
|
|||
|
|
<!-- GitHub Pages SEO -->
|
|||
|
|
<meta name="description" content="LockBit.chat - P2P мессенджер с военным уровнем криптографии и Lightning Network платежами">
|
|||
|
|
<meta name="keywords" content="P2P messenger, encryption, Lightning Network, WebRTC, privacy">
|
|||
|
|
<meta name="author" content="Volodymyr">
|
|||
|
|
<link rel="canonical" href="https://github.com/lockbitchat/lockbit-chat/">
|
|||
|
|
|
|||
|
|
<!-- Open Graph -->
|
|||
|
|
<meta property="og:title" content="LockBit.chat - Enhanced Security Edition">
|
|||
|
|
<meta property="og:description" content="Первый P2P мессенджер с Lightning Network платежами">
|
|||
|
|
<meta property="og:url" content="https://github.com/lockbitchat/lockbit-chat/">
|
|||
|
|
<meta property="og:type" content="website">
|
|||
|
|
<meta property="og:image" content="https://github.com/lockbitchat/lockbit-chat/assets/images/og-image.png">
|
|||
|
|
|
|||
|
|
<!-- Twitter Card -->
|
|||
|
|
<meta name="twitter:card" content="summary_large_image">
|
|||
|
|
<meta name="twitter:title" content="LockBit.chat - Enhanced Security Edition">
|
|||
|
|
<meta name="twitter:description" content="P2P мессенджер с военным уровнем криптографии">
|
|||
|
|
|
|||
|
|
<title>LockBit.chat - Enhanced Security Edition</title>
|
|||
|
|
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
|||
|
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
|||
|
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|||
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|||
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer">
|
|||
|
|
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
|
|||
|
|
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/webfonts/fa-regular-400.woff2" as="font" type="font/woff2" crossorigin>
|
|||
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|||
|
|
<link rel="stylesheet" href="src/styles/main.css">
|
|||
|
|
<link rel="stylesheet" href="src/styles/animations.css">
|
|||
|
|
<link rel="stylesheet" href="src/styles/components.css">
|
|||
|
|
<script>
|
|||
|
|
// Enhanced icon loading fallback
|
|||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
|
// Check if Font Awesome loaded properly
|
|||
|
|
function checkFontAwesome() {
|
|||
|
|
const testIcon = document.createElement('i');
|
|||
|
|
testIcon.className = 'fas fa-shield-halved';
|
|||
|
|
testIcon.style.position = 'absolute';
|
|||
|
|
testIcon.style.left = '-9999px';
|
|||
|
|
testIcon.style.visibility = 'hidden';
|
|||
|
|
document.body.appendChild(testIcon);
|
|||
|
|
|
|||
|
|
const computedStyle = window.getComputedStyle(testIcon, '::before');
|
|||
|
|
const content = computedStyle.content;
|
|||
|
|
const fontFamily = computedStyle.fontFamily;
|
|||
|
|
|
|||
|
|
document.body.removeChild(testIcon);
|
|||
|
|
|
|||
|
|
// Check if Font Awesome is properly loaded
|
|||
|
|
if (!content || content === 'none' || content === 'normal' ||
|
|||
|
|
!fontFamily.includes('Font Awesome') && !fontFamily.includes('fa-solid')) {
|
|||
|
|
console.warn('Font Awesome not loaded properly, using fallback icons');
|
|||
|
|
document.body.classList.add('fa-fallback');
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('Font Awesome loaded successfully');
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Check immediately and after a delay
|
|||
|
|
if (!checkFontAwesome()) {
|
|||
|
|
// Try alternative CDN if first one failed
|
|||
|
|
setTimeout(function() {
|
|||
|
|
if (!checkFontAwesome()) {
|
|||
|
|
console.warn('Font Awesome still not loaded, using fallback icons');
|
|||
|
|
document.body.classList.add('fa-fallback');
|
|||
|
|
}
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<div id="root"></div>
|
|||
|
|
|
|||
|
|
<script type="text/babel">
|
|||
|
|
|
|||
|
|
// Slider Component
|
|||
|
|
const UniqueFeatureSlider = () => {
|
|||
|
|
const [currentSlide, setCurrentSlide] = React.useState(0);
|
|||
|
|
|
|||
|
|
const slides = [
|
|||
|
|
{
|
|||
|
|
icon: "fas fa-shield-halved",
|
|||
|
|
color: "orange",
|
|||
|
|
title: "Военный уровень шифрования",
|
|||
|
|
description: "ECDH P-384 + AES-GCM 256-bit + ECDSA цифровые подписи обеспечивают криптографическую стойкость уровня NSA Suite B"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
icon: "fas fa-sync-alt",
|
|||
|
|
color: "purple",
|
|||
|
|
title: "Perfect Forward Secrecy",
|
|||
|
|
description: "Автоматическая ротация ключей каждые 5 минут гарантирует, что даже при компрометации одного ключа остальные сообщения останутся защищенными"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
icon: "fas fa-user-shield",
|
|||
|
|
color: "green",
|
|||
|
|
title: "Защита от MITM атак",
|
|||
|
|
description: "Out-of-band верификация с кодами безопасности + взаимная аутентификация исключают возможность атак человека посередине"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
icon: "fas fa-bolt",
|
|||
|
|
color: "yellow",
|
|||
|
|
title: "Lightning-платежи",
|
|||
|
|
description: "Оплата сессий в сатоши через Lightning Network. Приватность платежей + мгновенные микротранзакции без банков"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
icon: "fas fa-eye-slash",
|
|||
|
|
color: "blue",
|
|||
|
|
title: "Нулевое хранение данных",
|
|||
|
|
description: "Все сообщения существуют только в памяти браузера. Никаких серверов, логов или баз данных. Полная анонимность"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
icon: "fas fa-code",
|
|||
|
|
color: "cyan",
|
|||
|
|
title: "Open Source безопасность",
|
|||
|
|
description: "Весь код открыт для аудита. Криптография работает прямо в браузере без доверия к серверам или третьим сторонам"
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const nextSlide = () => setCurrentSlide((prev) => (prev + 1) % slides.length);
|
|||
|
|
const prevSlide = () => setCurrentSlide((prev) => (prev - 1 + slides.length) % slides.length);
|
|||
|
|
const goToSlide = (index) => setCurrentSlide(index);
|
|||
|
|
|
|||
|
|
React.useEffect(() => {
|
|||
|
|
const timer = setInterval(() => {
|
|||
|
|
nextSlide();
|
|||
|
|
}, 15000);
|
|||
|
|
return () => clearInterval(timer);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="mt-12">
|
|||
|
|
<div className="text-center mb-8">
|
|||
|
|
<h3 className="text-2xl font-semibold text-primary mb-3">
|
|||
|
|
Почему LockBit.chat уникальный
|
|||
|
|
</h3>
|
|||
|
|
<p className="text-secondary max-w-2xl mx-auto">
|
|||
|
|
Единственный мессенджер с военным уровнем криптографии и Lightning-платежами
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="relative max-w-4xl mx-auto">
|
|||
|
|
<div className="overflow-hidden rounded-xl">
|
|||
|
|
<div
|
|||
|
|
className="flex transition-transform duration-500 ease-in-out"
|
|||
|
|
style={{ transform: `translateX(-${currentSlide * 100}%)` }}
|
|||
|
|
>
|
|||
|
|
{slides.map((slide, index) => (
|
|||
|
|
<div key={index} className="w-full flex-shrink-0 px-4">
|
|||
|
|
<div className="card-minimal rounded-xl p-8 text-center min-h-[300px] flex flex-col justify-center relative overflow-hidden">
|
|||
|
|
{/* Фон-иконка */}
|
|||
|
|
<i
|
|||
|
|
className={`${slide.icon} absolute right-[-100px] top-1/2 -translate-y-1/2 opacity-10 text-[300px] pointer-events-none ${
|
|||
|
|
slide.color === 'orange' ? 'text-orange-500' :
|
|||
|
|
slide.color === 'purple' ? 'text-purple-500' :
|
|||
|
|
slide.color === 'green' ? 'text-green-500' :
|
|||
|
|
slide.color === 'yellow' ? 'text-yellow-500' :
|
|||
|
|
slide.color === 'blue' ? 'text-blue-500' :
|
|||
|
|
'text-cyan-500'
|
|||
|
|
}`}
|
|||
|
|
></i>
|
|||
|
|
|
|||
|
|
{/* Контент */}
|
|||
|
|
<h4 className="text-xl font-semibold text-primary mb-4 relative z-10">
|
|||
|
|
{slide.title}
|
|||
|
|
</h4>
|
|||
|
|
<p className="text-secondary leading-relaxed max-w-2xl mx-auto relative z-10">
|
|||
|
|
{slide.description}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Навигация */}
|
|||
|
|
<button
|
|||
|
|
onClick={prevSlide}
|
|||
|
|
className="absolute left-2 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-gray-600/80 hover:bg-gray-500/80 text-white rounded-full flex items-center justify-center transition-all duration-200 z-10"
|
|||
|
|
>
|
|||
|
|
<i className="fas fa-chevron-left"></i>
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={nextSlide}
|
|||
|
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-gray-600/80 hover:bg-gray-500/80 text-white rounded-full flex items-center justify-center transition-all duration-200 z-10"
|
|||
|
|
>
|
|||
|
|
<i className="fas fa-chevron-right"></i>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* Точки */}
|
|||
|
|
<div className="flex justify-center space-x-2 mt-6">
|
|||
|
|
{slides.map((_, index) => (
|
|||
|
|
<button
|
|||
|
|
key={index}
|
|||
|
|
onClick={() => goToSlide(index)}
|
|||
|
|
className={`w-3 h-3 rounded-full transition-all duration-200 ${
|
|||
|
|
index === currentSlide
|
|||
|
|
? 'bg-orange-500'
|
|||
|
|
: 'bg-gray-600 hover:bg-gray-500'
|
|||
|
|
}`}
|
|||
|
|
></button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const ComparisonTable = () => {
|
|||
|
|
const [selectedFeature, setSelectedFeature] = React.useState(null);
|
|||
|
|
|
|||
|
|
const messengers = [
|
|||
|
|
{
|
|||
|
|
name: "LockBit.chat",
|
|||
|
|
logo: React.createElement('div', {
|
|||
|
|
className: "w-8 h-8 bg-orange-500/10 border border-orange-500/20 rounded-lg flex items-center justify-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: 'fas fa-shield-halved text-orange-400'
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
type: "P2P WebRTC",
|
|||
|
|
color: "orange"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Signal",
|
|||
|
|
logo: React.createElement('svg', {
|
|||
|
|
key: 'signal-logo',
|
|||
|
|
className: "w-8 h-8",
|
|||
|
|
viewBox: "0 0 122.88 122.31",
|
|||
|
|
xmlns: "http://www.w3.org/2000/svg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('path', {
|
|||
|
|
key: 'bg',
|
|||
|
|
className: "fill-blue-500",
|
|||
|
|
d: "M27.75,0H95.13a27.83,27.83,0,0,1,27.75,27.75V94.57a27.83,27.83,0,0,1-27.75,27.74H27.75A27.83,27.83,0,0,1,0,94.57V27.75A27.83,27.83,0,0,1,27.75,0Z"
|
|||
|
|
}),
|
|||
|
|
React.createElement('path', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: "fill-white",
|
|||
|
|
d: "M61.44,25.39A35.76,35.76,0,0,0,31.18,80.18L27.74,94.86l14.67-3.44a35.77,35.77,0,1,0,19-66Z"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
type: "Centralized",
|
|||
|
|
color: "blue"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Threema",
|
|||
|
|
logo: React.createElement('svg', {
|
|||
|
|
key: 'threema-logo',
|
|||
|
|
className: "w-8 h-8",
|
|||
|
|
viewBox: "0 0 122.88 122.88",
|
|||
|
|
xmlns: "http://www.w3.org/2000/svg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('rect', {
|
|||
|
|
key: 'bg',
|
|||
|
|
width: "122.88",
|
|||
|
|
height: "122.88",
|
|||
|
|
rx: "18.43",
|
|||
|
|
fill: "#474747"
|
|||
|
|
}),
|
|||
|
|
React.createElement('path', {
|
|||
|
|
key: 'bubble',
|
|||
|
|
fill: "#FFFFFF",
|
|||
|
|
d: "M44.26,78.48l-19.44,4.8l4.08-16.56c-4.08-5.28-6.48-12-6.48-18.96c0-18.96,17.52-34.32,39.12-34.32 c21.6,0,39.12,15.36,39.12,34.32c0,18.96-17.52,34.32-39.12,34.32c-6,0-12-1.2-17.04-3.36L44.26,78.48z M50.26,44.64 h-0.48c-0.96,0-1.68,0.72-1.44,1.68v15.6c0,0.96,0.72,1.68,1.68,1.68l23.04,0c0.96,0,1.68-0.72,1.68-1.68v-15.6 c0-0.96-0.72-1.68-1.68-1.68h-0.48v-4.32c0-6-5.04-11.04-11.04-11.04S50.5,34.32,50.5,40.32v4.32H50.26z M68.02,44.64 h-13.2v-4.32c0-3.6,2.88-6.72,6.72-6.72c3.6,0,6.72,2.88,6.72,6.72v4.32H68.02z"
|
|||
|
|
}),
|
|||
|
|
React.createElement('circle', {
|
|||
|
|
key: 'dot1',
|
|||
|
|
cx: "37.44",
|
|||
|
|
cy: "97.44",
|
|||
|
|
r: "6.72",
|
|||
|
|
fill: "#3fe669"
|
|||
|
|
}),
|
|||
|
|
React.createElement('circle', {
|
|||
|
|
key: 'dot2',
|
|||
|
|
cx: "61.44",
|
|||
|
|
cy: "97.44",
|
|||
|
|
r: "6.72",
|
|||
|
|
fill: "#3fe669"
|
|||
|
|
}),
|
|||
|
|
React.createElement('circle', {
|
|||
|
|
key: 'dot3',
|
|||
|
|
cx: "85.44",
|
|||
|
|
cy: "97.44",
|
|||
|
|
r: "6.72",
|
|||
|
|
fill: "#3fe669"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
type: "Centralized",
|
|||
|
|
color: "green"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Olvid",
|
|||
|
|
logo: React.createElement('svg', {
|
|||
|
|
key: 'olvid-logo',
|
|||
|
|
className: "w-8 h-8",
|
|||
|
|
viewBox: "0 0 128 128",
|
|||
|
|
xmlns: "http://www.w3.org/2000/svg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('g', {
|
|||
|
|
key: 'group',
|
|||
|
|
transform: "translate(0,128) scale(0.1,-0.1)",
|
|||
|
|
fill: "#0a4bd1",
|
|||
|
|
stroke: "none"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('path', {
|
|||
|
|
key: 'p1',
|
|||
|
|
d: "M262 1264 c-139 -37 -217 -120 -247 -260 -18 -85 -21 -627 -4 -720 21 -115 89 -201 198 -253 l56 -26 370 0 c284 0 379 3 410 13 108 37 203 140 225 247 7 36 10 169 8 402 -4 396 -6 405 -85 493 -50 55 -100 86 -178 106 -73 20 -679 18 -753 -2z m538 -215 c299 -105 388 -490 165 -714 -216 -216 -598 -152 -728 122 -30 64 -32 74 -32 183 0 114 1 117 38 192 99 201 337 294 557 217z"
|
|||
|
|
}),
|
|||
|
|
React.createElement('path', {
|
|||
|
|
key: 'p2',
|
|||
|
|
d: "M550 864 c-46 -20 -87 -59 -113 -109 -17 -31 -22 -58 -22 -115 0 -85 15 -122 72 -182 20 -20 32 -41 28 -47 -3 -6 -30 -25 -58 -41 l-52 -30 70 0 c137 0 262 46 329 120 51 57 69 103 69 181 0 106 -51 188 -140 226 -43 17 -139 16 -183 -3z"
|
|||
|
|
})
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
type: "Centralized",
|
|||
|
|
color: "purple"
|
|||
|
|
}
|
|||
|
|
,
|
|||
|
|
{
|
|||
|
|
name: "Session",
|
|||
|
|
logo: React.createElement('svg', {
|
|||
|
|
key: 'olvid-logo',
|
|||
|
|
className: "w-8 h-8",
|
|||
|
|
viewBox: "0 0 1024 1024",
|
|||
|
|
xmlns: "http://www.w3.org/2000/svg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('rect', {
|
|||
|
|
key: 'bg',
|
|||
|
|
width: "1024",
|
|||
|
|
height: "1024",
|
|||
|
|
fill: "#333132"
|
|||
|
|
}),
|
|||
|
|
React.createElement('path', {
|
|||
|
|
key: 'icon',
|
|||
|
|
fill: "#00f782",
|
|||
|
|
d: "M431 574.8c-.8-7.4-6.7-8.2-10.8-10.6-13.6-7.9-27.5-15.4-41.3-23l-22.5-12.3c-8.5-4.7-17.1-9.2-25.6-14.1-10.5-6-21-11.9-31.1-18.6-18.9-12.5-33.8-29.1-46.3-48.1-8.3-12.6-14.8-26.1-19.2-40.4-6.7-21.7-10.8-44.1-7.8-66.8 1.8-14 4.6-28 9.7-41.6 7.8-20.8 19.3-38.8 34.2-54.8 9.8-10.6 21.2-19.1 33.4-26.8 14.7-9.3 30.7-15.4 47.4-19 13.8-3 28.1-4.3 42.2-4.4 89.9-.4 179.7-.3 269.6 0 12.6 0 25.5 1 37.7 4.1 24.3 6.2 45.7 18.2 63 37 11.2 12.2 20.4 25.8 25.8 41.2 7.3 20.7 12.3 42.1 6.7 64.4-2.1 8.5-2.7 17.5-6.1 25.4-4.7 10.9-10.8 21.2-17.2 31.2-8.7 13.5-20.5 24.3-34.4 32.2-10.1 5.7-21 10.2-32 14.3-18.1 6.7-37.2 5-56.1 5.2-17.2.2-34.5 0-51.7.1-1.7 0-3.4 1.2-5.1 1.9 1.3 1.8 2.1 4.3 3.9 5.3 13.5 7.8 27.2 15.4 40.8 22.9 11 6 22.3 11.7 33.2 17.9 15.2 8.5 30.2 17.4 45.3 26.1 19.3 11.1 34.8 26.4 47.8 44.3 9.7 13.3 17.2 27.9 23 43.5 6.1 16.6 9.2 33.8 10.4 51.3.6 9.1-.7 18.5-1.9 27.6-1.2 9.1-2.7 18.4-5.6 27.1-3.3 10.2-7.4 20.2-12.4 29.6-8.4 15.7-19.6 29.4-32.8 41.4-12.7 11.5-26.8 20.6-42.4 27.6-22.9 10.3-46.9 14.4-71.6 14.5-89.7.3-179.4.2-269.1-.1-12.6 0-25.5-1-37.7-3.9-24.5-5.7-45.8-18-63.3-36.4-11.6-12.3-20.2-26.5-26.6-41.9-2.7-6.4-4.1-13.5-5.4-20.4-1.5-8.1-2.8-16.3-3.1-24.5-.6-15.7 2.8-30.9 8.2-45.4 8.2-22 21.7-40.6 40.2-55.2 10-7.9 21.3-13.7 33.1-18.8 16.6-7.2 34-8.1 51.4-8.5 21.9-.5 43.9-.1 65.9-.1 1.9-.1 3.9-.3 6.2-.4zm96.3-342.4c0 .1 0 .1 0 0-48.3.1-96.6-.6-144.9.5-13.5.3-27.4 3.9-40.1 8.7-14.9 5.6-28.1 14.6-39.9 25.8-20.2 19-32.2 42.2-37.2 68.9-3.6 19-1.4 38.1 4.1 56.5 4.1 13.7 10.5 26.4 18.5 38.4 14.8 22.2 35.7 36.7 58.4 49.2 11 6.1 22.2 11.9 33.2 18 13.5 7.5 26.9 15.1 40.4 22.6 13.1 7.3 26.2 14.5 39.2 21.7 9.7 5.3 19.4 10.7 29.1 16.1 2.9 1.6 4.1.2 4.5-2.4.3-2 .3-4 .3-6.1v-58.8c0-19.9.1-39.9 0-59.8 0-6.6 1.7-12.8 7.6-16.1 3.5-2 8.2-2.8 12.4-2.8 50.3-.2 100.7-.2 151-.1 19.8 0 38.3-4.4 55.1-15.1 23.1-14.8 36.3-36.3 40.6-62.9 3.4-20.8-1-40.9-12.4-58.5-17.8-27.5-43.6-43-76.5-43.6-47.8-.8-95.6-.2-143.4-.2zm-30.6 559.7c45.1 0 90.2-.2 135.3.1 18.9.1 36.6-3.9 53.9-11.1 18.4-7.7 33.6-19.8 46.3-34.9 9.1-10.8 16.2-22.9 20.8-36.5 4.2-12.4 7.4-24.7 7.3-37.9-.1-10.3.2-20.5-3.4-30.5-2.6-7.2-3.4-15.2-6.4-22.1-3.9-8.9-8.9-17.3-14-25.5-12.9-20.8-31.9-34.7-52.8-46.4-10.6-5.9-21.2-11.6-31.8-17.5-10.3-5.7-20.4-11.7-30.7-17.4-11.2-6.1-22.5-11.9-33.7-18-16.6-9.1-33.1-18.4-49.8-27.5-4.9-2.7-6.1-1.9-6.4 3.9-.1 2-.1 4.1-.1 6.1v114.5c0 14.8-5.6 20.4-20.4 20.4-47.6.1-95.3-.1-142.9.2-10.5.1-21.1 1.4-31.6 2.8-16.5 2.2-30.5 9.9-42.8 21-17 15.5-27 34.7-29.4 57.5-1.1 10.9-.4 21.7 2.9 32.5 3.7 12.3 9.2 23.4 17.5 33 19.2 22.1 43.4 33.3 72.7 33.3 46.6.1 93 0 139.5 0z"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
type: "Onion Network",
|
|||
|
|
color: "cyan"
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const features = [
|
|||
|
|
{
|
|||
|
|
name: "Криптография",
|
|||
|
|
lockbit: { status: "🏆", detail: "ECDH P-384 + AES-GCM 256 + ECDSA" },
|
|||
|
|
signal: { status: "✅", detail: "Signal Protocol + Double Ratchet" },
|
|||
|
|
threema: { status: "✅", detail: "NaCl + XSalsa20 + Poly1305" },
|
|||
|
|
olvid: { status: "✅", detail: "OlvidEngine + пост-квантовая" },
|
|||
|
|
session: { status: "✅", detail: "Signal Protocol модифицированный" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Perfect Forward Secrecy",
|
|||
|
|
lockbit: { status: "🏆", detail: "Автоматическая ротация каждые 5 мин" },
|
|||
|
|
signal: { status: "✅", detail: "Double Ratchet алгоритм" },
|
|||
|
|
threema: { status: "⚠️", detail: "Частично (групповые чаты)" },
|
|||
|
|
olvid: { status: "✅", detail: "Полная PFS для всех сообщений" },
|
|||
|
|
session: { status: "✅", detail: "Session Ratchet алгоритм" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Архитектура",
|
|||
|
|
lockbit: { status: "🏆", detail: "Чистый P2P без серверов" },
|
|||
|
|
signal: { status: "❌", detail: "Централизованные серверы Signal" },
|
|||
|
|
threema: { status: "❌", detail: "Серверы Threema в Швейцарии" },
|
|||
|
|
olvid: { status: "❌", detail: "Серверы во Франции" },
|
|||
|
|
session: { status: "⚠️", detail: "Onion routing через узлы сети" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Анонимность регистрации",
|
|||
|
|
lockbit: { status: "🏆", detail: "Регистрация не требуется" },
|
|||
|
|
signal: { status: "❌", detail: "Обязателен номер телефона" },
|
|||
|
|
threema: { status: "✅", detail: "ID генерируется локально" },
|
|||
|
|
olvid: { status: "✅", detail: "Криптографическая идентичность" },
|
|||
|
|
session: { status: "✅", detail: "Случайный Session ID" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Защита метаданных",
|
|||
|
|
lockbit: { status: "🏆", detail: "Полное шифрование метаданных" },
|
|||
|
|
signal: { status: "⚠️", detail: "Sealed Sender (частично)" },
|
|||
|
|
threema: { status: "⚠️", detail: "Минимальные метаданные" },
|
|||
|
|
olvid: { status: "✅", detail: "Защита через обфускацию" },
|
|||
|
|
session: { status: "✅", detail: "Onion routing скрывает метаданные" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Открытый код",
|
|||
|
|
lockbit: { status: "🏆", detail: "100% открытый и аудируемый" },
|
|||
|
|
signal: { status: "✅", detail: "Полностью открытый" },
|
|||
|
|
threema: { status: "⚠️", detail: "Только клиенты открыты" },
|
|||
|
|
olvid: { status: "✅", detail: "Полностью открытый" },
|
|||
|
|
session: { status: "✅", detail: "Полностью открытый" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Цифровые подписи",
|
|||
|
|
lockbit: { status: "🏆", detail: "ECDSA P-384 встроенные" },
|
|||
|
|
signal: { status: "✅", detail: "Ed25519 подписи" },
|
|||
|
|
threema: { status: "✅", detail: "Ed25519 подписи" },
|
|||
|
|
olvid: { status: "✅", detail: "Множественные алгоритмы" },
|
|||
|
|
session: { status: "✅", detail: "Ed25519 подписи" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Экономическая модель",
|
|||
|
|
lockbit: { status: "🏆", detail: "Lightning сатоши за сессию" },
|
|||
|
|
signal: { status: "⚠️", detail: "Пожертвования и гранты" },
|
|||
|
|
threema: { status: "✅", detail: "Разовая покупка приложения" },
|
|||
|
|
olvid: { status: "✅", detail: "Бесплатно + Enterprise" },
|
|||
|
|
session: { status: "⚠️", detail: "Пожертвования" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Устойчивость к цензуре",
|
|||
|
|
lockbit: { status: "🏆", detail: "Невозможно заблокировать P2P" },
|
|||
|
|
signal: { status: "⚠️", detail: "Блокируется в авторитарных странах" },
|
|||
|
|
threema: { status: "⚠️", detail: "Может быть заблокирован" },
|
|||
|
|
olvid: { status: "⚠️", detail: "Может быть заблокирован" },
|
|||
|
|
session: { status: "✅", detail: "Onion routing обходит блокировки" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Хранение данных",
|
|||
|
|
lockbit: { status: "🏆", detail: "Только в памяти браузера" },
|
|||
|
|
signal: { status: "⚠️", detail: "Локальная база данных" },
|
|||
|
|
threema: { status: "⚠️", detail: "Локально + опциональный бэкап" },
|
|||
|
|
olvid: { status: "✅", detail: "Локально без облачного бэкапа" },
|
|||
|
|
session: { status: "⚠️", detail: "Локальная база данных" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Пост-квантовая защита",
|
|||
|
|
lockbit: { status: "⚠️", detail: "Планируется в версии 5.0" },
|
|||
|
|
signal: { status: "⚠️", detail: "В разработке PQXDH" },
|
|||
|
|
threema: { status: "❌", detail: "Не реализовано" },
|
|||
|
|
olvid: { status: "🏆", detail: "Уже реализована" },
|
|||
|
|
session: { status: "❌", detail: "Не реализовано" }
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "Верификация MITM",
|
|||
|
|
lockbit: { status: "🏆", detail: "Out-of-band коды + взаимная auth" },
|
|||
|
|
signal: { status: "✅", detail: "Safety numbers" },
|
|||
|
|
threema: { status: "✅", detail: "Сканирование QR кодов" },
|
|||
|
|
olvid: { status: "✅", detail: "SAS верификация" },
|
|||
|
|
session: { status: "⚠️", detail: "Базовая верификация ключей" }
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const getStatusIcon = (status) => {
|
|||
|
|
switch (status) {
|
|||
|
|
case "🏆": return { icon: status, color: "text-yellow-400" };
|
|||
|
|
case "✅": return { icon: status, color: "text-green-400" };
|
|||
|
|
case "⚠️": return { icon: status, color: "text-yellow-400" };
|
|||
|
|
case "❌": return { icon: status, color: "text-red-400" };
|
|||
|
|
default: return { icon: status, color: "text-gray-400" };
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const toggleFeatureDetail = (index) => {
|
|||
|
|
setSelectedFeature(selectedFeature === index ? null : index);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
key: 'comparison-section',
|
|||
|
|
className: "mt-16"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'section-header',
|
|||
|
|
className: "text-center mb-8"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-2xl font-semibold text-primary mb-3"
|
|||
|
|
}, 'Сравнение с лидерами приватности'),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'subtitle',
|
|||
|
|
className: "text-secondary max-w-2xl mx-auto mb-4"
|
|||
|
|
}, "Детальное сравнение с самыми защищенными мессенджерами мира"),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'trophy-note',
|
|||
|
|
className: "inline-flex items-center px-4 py-2 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'trophy',
|
|||
|
|
className: "text-yellow-400 mr-2"
|
|||
|
|
}, "🏆"),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'text',
|
|||
|
|
className: "text-yellow-300 text-sm font-medium"
|
|||
|
|
}, "Лидер в категории")
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'table-container',
|
|||
|
|
className: "max-w-7xl mx-auto"
|
|||
|
|
}, [
|
|||
|
|
// Mobile warning
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'mobile-warning',
|
|||
|
|
className: "md:hidden p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('p', {
|
|||
|
|
className: "text-yellow-400 text-sm text-center"
|
|||
|
|
}, "💡 Поверните устройство горизонтально для лучшего просмотра")
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Table
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'table-wrapper',
|
|||
|
|
className: "overflow-x-auto custom-scrollbar"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('table', {
|
|||
|
|
key: 'comparison-table',
|
|||
|
|
className: "w-full border-collapse rounded-xl overflow-hidden",
|
|||
|
|
style: { backgroundColor: "rgba(42, 43, 42, 0.8)" }
|
|||
|
|
}, [
|
|||
|
|
// Header
|
|||
|
|
React.createElement('thead', {
|
|||
|
|
key: 'table-head'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('tr', {
|
|||
|
|
key: 'header-row',
|
|||
|
|
className: "bg-header"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('th', {
|
|||
|
|
key: 'feature-header',
|
|||
|
|
className: "text-left p-4 border-b border-gray-600 text-primary font-semibold min-w-[220px]"
|
|||
|
|
}, 'Критерий безопасности'),
|
|||
|
|
...messengers.map((messenger, index) =>
|
|||
|
|
React.createElement('th', {
|
|||
|
|
key: `messenger-${index}`,
|
|||
|
|
className: "text-center p-4 border-b border-gray-600 min-w-[140px]"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'messenger-info',
|
|||
|
|
className: "flex flex-col items-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'logo',
|
|||
|
|
className: "mb-2"
|
|||
|
|
}, messenger.logo),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'name',
|
|||
|
|
className: `text-sm font-semibold ${
|
|||
|
|
messenger.color === 'orange' ? 'text-orange-400' :
|
|||
|
|
messenger.color === 'blue' ? 'text-blue-400' :
|
|||
|
|
messenger.color === 'green' ? 'text-green-400' :
|
|||
|
|
messenger.color === 'purple' ? 'text-purple-400' :
|
|||
|
|
'text-cyan-400'
|
|||
|
|
}`
|
|||
|
|
}, messenger.name),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'type',
|
|||
|
|
className: "text-xs text-gray-400"
|
|||
|
|
}, messenger.type)
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
)
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Body
|
|||
|
|
React.createElement('tbody', {
|
|||
|
|
key: 'table-body'
|
|||
|
|
}, features.map((feature, featureIndex) => [
|
|||
|
|
React.createElement('tr', {
|
|||
|
|
key: `feature-${featureIndex}`,
|
|||
|
|
className: `border-b border-gray-700/30 hover:bg-gray-800/20 transition-colors cursor-pointer ${
|
|||
|
|
selectedFeature === featureIndex ? 'bg-gray-800/40' : ''
|
|||
|
|
}`,
|
|||
|
|
onClick: () => toggleFeatureDetail(featureIndex)
|
|||
|
|
}, [
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'feature-name',
|
|||
|
|
className: "p-4 text-primary font-medium"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'feature-text',
|
|||
|
|
className: "flex items-center justify-between"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'name'
|
|||
|
|
}, feature.name),
|
|||
|
|
React.createElement('i', {
|
|||
|
|
key: 'expand-icon',
|
|||
|
|
className: `fas fa-chevron-${selectedFeature === featureIndex ? 'up' : 'down'} text-xs text-gray-400 opacity-50`
|
|||
|
|
})
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'lockbit-cell',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'lockbit-status',
|
|||
|
|
className: getStatusIcon(feature.lockbit.status).color + " text-xl"
|
|||
|
|
}, getStatusIcon(feature.lockbit.status).icon)
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'signal-cell',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'signal-status',
|
|||
|
|
className: getStatusIcon(feature.signal.status).color + " text-xl"
|
|||
|
|
}, getStatusIcon(feature.signal.status).icon)
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'threema-cell',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'threema-status',
|
|||
|
|
className: getStatusIcon(feature.threema.status).color + " text-xl"
|
|||
|
|
}, getStatusIcon(feature.threema.status).icon)
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'olvid-cell',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'olvid-status',
|
|||
|
|
className: getStatusIcon(feature.olvid.status).color + " text-xl"
|
|||
|
|
}, getStatusIcon(feature.olvid.status).icon)
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'session-cell',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'session-status',
|
|||
|
|
className: getStatusIcon(feature.session.status).color + " text-xl"
|
|||
|
|
}, getStatusIcon(feature.session.status).icon)
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Expanded details row
|
|||
|
|
selectedFeature === featureIndex && React.createElement('tr', {
|
|||
|
|
key: `details-${featureIndex}`,
|
|||
|
|
className: "border-b border-gray-700/30 bg-gray-800/10"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'details-spacer',
|
|||
|
|
className: "p-4 text-xs text-gray-400"
|
|||
|
|
}, "Детали:"),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'lockbit-detail',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'detail',
|
|||
|
|
className: "text-xs text-orange-300 font-medium leading-relaxed"
|
|||
|
|
}, feature.lockbit.detail)
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'signal-detail',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'detail',
|
|||
|
|
className: "text-xs text-blue-300 leading-relaxed"
|
|||
|
|
}, feature.signal.detail)
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'threema-detail',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'detail',
|
|||
|
|
className: "text-xs text-green-300 leading-relaxed"
|
|||
|
|
}, feature.threema.detail)
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'olvid-detail',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'detail',
|
|||
|
|
className: "text-xs text-purple-300 leading-relaxed"
|
|||
|
|
}, feature.olvid.detail)
|
|||
|
|
]),
|
|||
|
|
React.createElement('td', {
|
|||
|
|
key: 'session-detail',
|
|||
|
|
className: "p-4 text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'detail',
|
|||
|
|
className: "text-xs text-cyan-300 leading-relaxed"
|
|||
|
|
}, feature.session.detail)
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]).flat())
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Enhanced Legend
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'legend',
|
|||
|
|
className: "mt-8 grid grid-cols-2 md:grid-cols-4 gap-4 max-w-4xl mx-auto"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'legend-item-1',
|
|||
|
|
className: "flex items-center justify-center p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'icon1',
|
|||
|
|
className: "text-yellow-400 mr-2 text-lg"
|
|||
|
|
}, "🏆"),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'text1',
|
|||
|
|
className: "text-yellow-300 text-sm font-medium"
|
|||
|
|
}, "Лидер")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'legend-item-2',
|
|||
|
|
className: "flex items-center justify-center p-3 bg-green-500/10 border border-green-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'icon2',
|
|||
|
|
className: "text-green-400 mr-2 text-lg"
|
|||
|
|
}, "✅"),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'text2',
|
|||
|
|
className: "text-green-300 text-sm font-medium"
|
|||
|
|
}, "Отлично")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'legend-item-3',
|
|||
|
|
className: "flex items-center justify-center p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'icon3',
|
|||
|
|
className: "text-yellow-400 mr-2 text-lg"
|
|||
|
|
}, "⚠️"),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'text3',
|
|||
|
|
className: "text-yellow-300 text-sm font-medium"
|
|||
|
|
}, "Частично")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'legend-item-4',
|
|||
|
|
className: "flex items-center justify-center p-3 bg-red-500/10 border border-red-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'icon4',
|
|||
|
|
className: "text-red-400 mr-2 text-lg"
|
|||
|
|
}, "❌"),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'text4',
|
|||
|
|
className: "text-red-300 text-sm font-medium"
|
|||
|
|
}, "Нет")
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Summary
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'summary',
|
|||
|
|
className: "mt-8 p-6 bg-orange-500/10 border border-orange-500/20 rounded-xl max-w-4xl mx-auto"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('h4', {
|
|||
|
|
key: 'summary-title',
|
|||
|
|
className: "text-lg font-semibold text-orange-400 mb-3 flex items-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: 'fas fa-trophy mr-2'
|
|||
|
|
}),
|
|||
|
|
'Итоги сравнения'
|
|||
|
|
]),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'summary-text',
|
|||
|
|
className: "text-secondary leading-relaxed"
|
|||
|
|
}, "LockBit.chat лидирует в 8 из 12 категорий безопасности, особенно превосходя конкурентов в архитектуре P2P, анонимности и защите метаданных. Только Olvid опережает в пост-квантовой криптографии, тогда как централизованные решения показывают существенные недостатки в приватности.")
|
|||
|
|
])
|
|||
|
|
]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function Roadmap() {
|
|||
|
|
const [selectedPhase, setSelectedPhase] = React.useState(null);
|
|||
|
|
|
|||
|
|
const phases = [
|
|||
|
|
{
|
|||
|
|
version: "v1.0",
|
|||
|
|
title: "Начало разработки",
|
|||
|
|
status: "done",
|
|||
|
|
date: "Начало 2025",
|
|||
|
|
description: "Идея, прототип и настройка инфраструктуры",
|
|||
|
|
features: [
|
|||
|
|
"Формирование концепции и требований",
|
|||
|
|
"Выбор стека: WebRTC, P2P, криптография",
|
|||
|
|
"Первые прототипы обмена сообщениями",
|
|||
|
|
"Создание репозитория и CI",
|
|||
|
|
"Базовая архитектура шифрования",
|
|||
|
|
"Проектирование UX/UI"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v1.5",
|
|||
|
|
title: "Alpha Release",
|
|||
|
|
status: "done",
|
|||
|
|
date: "Весна 2025",
|
|||
|
|
description: "Первая публичная альфа: базовый чат и обмен ключами",
|
|||
|
|
features: [
|
|||
|
|
"Базовый P2P обмен сообщениями через WebRTC",
|
|||
|
|
"Простейшая E2E-шифровка (демо-схема)",
|
|||
|
|
"Стабильный сигналинг и переподключение",
|
|||
|
|
"Минимальный UX для тестирования",
|
|||
|
|
"Сбор обратной связи от ранних тестировщиков"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v2.0",
|
|||
|
|
title: "Security Hardened",
|
|||
|
|
status: "done",
|
|||
|
|
date: "Лето 2025",
|
|||
|
|
description: "Усиление безопасности и выпуск стабильной ветки",
|
|||
|
|
features: [
|
|||
|
|
"Внедрение ECDH/ECDSA в продакшн",
|
|||
|
|
"Perfect Forward Secrecy и ротация ключей",
|
|||
|
|
"Улучшенные проверки подлинности",
|
|||
|
|
"Шифрование файлов и передач больших payload'ов",
|
|||
|
|
"Аудит базовых криптопроцессов"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v3.0",
|
|||
|
|
title: "Scaling & Stability",
|
|||
|
|
status: "done",
|
|||
|
|
date: "Осень 2025",
|
|||
|
|
description: "Масштабирование сети и улучшение стабильности",
|
|||
|
|
features: [
|
|||
|
|
"Оптимизация P2P соединений и NAT traversal",
|
|||
|
|
"Механизмы повторного подключения и очереди сообщений",
|
|||
|
|
"Снижение потребления батареи на мобильных",
|
|||
|
|
"Поддержка синхронизации между устройствами",
|
|||
|
|
"Инструменты мониторинга и логирования для разработчиков"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v3.5",
|
|||
|
|
title: "Privacy-first Release",
|
|||
|
|
status: "done",
|
|||
|
|
date: "Зима 2025",
|
|||
|
|
description: "Фокус на приватности: минимизация метаданных",
|
|||
|
|
features: [
|
|||
|
|
"Защита метаданных и уменьшение fingerprint'а",
|
|||
|
|
"Эксперименты с onion-routing и DHT",
|
|||
|
|
"Опции анонимных соединений",
|
|||
|
|
"Подготовка к открытой ревизии кода",
|
|||
|
|
"Улучшенные процессы верификации пользователей"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// current and future phases (kept from original roadmap)
|
|||
|
|
{
|
|||
|
|
version: "v4.0",
|
|||
|
|
title: "Enhanced Security Edition",
|
|||
|
|
status: "current",
|
|||
|
|
date: "Сейчас",
|
|||
|
|
description: "Текущая версия с военным уровнем криптографии",
|
|||
|
|
features: [
|
|||
|
|
"ECDH P-384 + AES-GCM 256-bit шифрование",
|
|||
|
|
"ECDSA цифровые подписи",
|
|||
|
|
"Perfect Forward Secrecy с ротацией ключей",
|
|||
|
|
"Out-of-band верификация MITM",
|
|||
|
|
"Lightning Network платежи",
|
|||
|
|
"P2P WebRTC архитектура",
|
|||
|
|
"Защита метаданных",
|
|||
|
|
"100% открытый код"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v4.5",
|
|||
|
|
title: "Mobile & Desktop Edition",
|
|||
|
|
status: "development",
|
|||
|
|
date: "Q2 2025",
|
|||
|
|
description: "Нативные приложения для всех платформ",
|
|||
|
|
features: [
|
|||
|
|
"PWA приложение для мобильных",
|
|||
|
|
"Electron приложение для десктопа",
|
|||
|
|
"Уведомления в реальном времени",
|
|||
|
|
"Автоматическое переподключение",
|
|||
|
|
"Оптимизация батареи",
|
|||
|
|
"Синхронизация между устройствами",
|
|||
|
|
"Улучшенный UX/UI",
|
|||
|
|
"Поддержка файлов до 100MB"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v5.0",
|
|||
|
|
title: "Quantum-Resistant Edition",
|
|||
|
|
status: "planned",
|
|||
|
|
date: "Q4 2025",
|
|||
|
|
description: "Защита от квантовых компьютеров",
|
|||
|
|
features: [
|
|||
|
|
"Пост-квантовая криптография CRYSTALS-Kyber",
|
|||
|
|
"SPHINCS+ цифровые подписи",
|
|||
|
|
"Гибридная схема: классика + PQ",
|
|||
|
|
"Quantum-safe key exchange",
|
|||
|
|
"Обновленные алгоритмы хеширования",
|
|||
|
|
"Миграция существующих сессий",
|
|||
|
|
"Совместимость с v4.x",
|
|||
|
|
"Квантово-стойкие протоколы"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v5.5",
|
|||
|
|
title: "Group Communications",
|
|||
|
|
status: "planned",
|
|||
|
|
date: "Q2 2026",
|
|||
|
|
description: "Групповые чаты с сохранением приватности",
|
|||
|
|
features: [
|
|||
|
|
"Групповые P2P соединения до 8 участников",
|
|||
|
|
"Mesh networking для групп",
|
|||
|
|
"Signal Double Ratchet для групп",
|
|||
|
|
"Анонимные группы без метаданных",
|
|||
|
|
"Эфемерные группы (исчезают после сессии)",
|
|||
|
|
"Групповые Lightning платежи",
|
|||
|
|
"Администрирование через криптографию",
|
|||
|
|
"Аудит участников группы"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v6.0",
|
|||
|
|
title: "Decentralized Network",
|
|||
|
|
status: "research",
|
|||
|
|
date: "2027",
|
|||
|
|
description: "Полностью децентрализованная сеть",
|
|||
|
|
features: [
|
|||
|
|
"Mesh сеть LockBit узлов",
|
|||
|
|
"DHT для обнаружения пиров",
|
|||
|
|
"Onion routing встроенный",
|
|||
|
|
"Токеномика и стимулы узлов",
|
|||
|
|
"Governance через DAO",
|
|||
|
|
"Interoperability с другими сетями",
|
|||
|
|
"Кроссплатформенная совместимость",
|
|||
|
|
"Самовосстанавливающаяся сеть"
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
version: "v7.0",
|
|||
|
|
title: "AI Privacy Assistant",
|
|||
|
|
status: "research",
|
|||
|
|
date: "2028+",
|
|||
|
|
description: "ИИ для приватности и безопасности",
|
|||
|
|
features: [
|
|||
|
|
"Локальный ИИ анализ угроз",
|
|||
|
|
"Автоматическое обнаружение MITM",
|
|||
|
|
"Адаптивная криптография",
|
|||
|
|
"Personalized security recommendations",
|
|||
|
|
"Zero-knowledge machine learning",
|
|||
|
|
"Приватный ИИ-ассистент",
|
|||
|
|
"Предиктивная безопасность",
|
|||
|
|
"Автономная защита от атак"
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
const getStatusConfig = (status) => {
|
|||
|
|
switch (status) {
|
|||
|
|
case 'current':
|
|||
|
|
return {
|
|||
|
|
color: 'green',
|
|||
|
|
bgClass: 'bg-green-500/10 border-green-500/20',
|
|||
|
|
textClass: 'text-green-400',
|
|||
|
|
icon: 'fas fa-check-circle',
|
|||
|
|
label: 'Текущая версия'
|
|||
|
|
};
|
|||
|
|
case 'development':
|
|||
|
|
return {
|
|||
|
|
color: 'orange',
|
|||
|
|
bgClass: 'bg-orange-500/10 border-orange-500/20',
|
|||
|
|
textClass: 'text-orange-400',
|
|||
|
|
icon: 'fas fa-code',
|
|||
|
|
label: 'В разработке'
|
|||
|
|
};
|
|||
|
|
case 'planned':
|
|||
|
|
return {
|
|||
|
|
color: 'blue',
|
|||
|
|
bgClass: 'bg-blue-500/10 border-blue-500/20',
|
|||
|
|
textClass: 'text-blue-400',
|
|||
|
|
icon: 'fas fa-calendar-alt',
|
|||
|
|
label: 'Запланировано'
|
|||
|
|
};
|
|||
|
|
case 'research':
|
|||
|
|
return {
|
|||
|
|
color: 'purple',
|
|||
|
|
bgClass: 'bg-purple-500/10 border-purple-500/20',
|
|||
|
|
textClass: 'text-purple-400',
|
|||
|
|
icon: 'fas fa-flask',
|
|||
|
|
label: 'Исследования'
|
|||
|
|
};
|
|||
|
|
case 'done':
|
|||
|
|
return {
|
|||
|
|
color: 'gray',
|
|||
|
|
bgClass: 'bg-gray-500/10 border-gray-500/20',
|
|||
|
|
textClass: 'text-gray-300',
|
|||
|
|
icon: 'fas fa-flag-checkered',
|
|||
|
|
label: 'Выпущено'
|
|||
|
|
};
|
|||
|
|
default:
|
|||
|
|
return {
|
|||
|
|
color: 'gray',
|
|||
|
|
bgClass: 'bg-gray-500/10 border-gray-500/20',
|
|||
|
|
textClass: 'text-gray-400',
|
|||
|
|
icon: 'fas fa-question',
|
|||
|
|
label: 'Неизвестно'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const togglePhaseDetail = (index) => {
|
|||
|
|
setSelectedPhase(selectedPhase === index ? null : index);
|
|||
|
|
};
|
|||
|
|
return (
|
|||
|
|
<div key="roadmap-section" className="mt-16 px-4 sm:px-0">
|
|||
|
|
<div key="section-header" className="text-center mb-12">
|
|||
|
|
<h3 key="title" className="text-2xl font-semibold text-primary mb-3">
|
|||
|
|
Roadmap развития
|
|||
|
|
</h3>
|
|||
|
|
<p key="subtitle" className="text-secondary max-w-2xl mx-auto mb-6">
|
|||
|
|
Эволюция LockBit.chat: от начальной разработки до квантово-стойкой
|
|||
|
|
децентрализованной сети
|
|||
|
|
</p>
|
|||
|
|
<div
|
|||
|
|
key="roadmap-note"
|
|||
|
|
className="inline-flex items-center px-4 py-2 bg-blue-500/10 border border-blue-500/20 rounded-lg"
|
|||
|
|
>
|
|||
|
|
<i key="icon" className="fas fa-rocket text-blue-400 mr-2" />
|
|||
|
|
<span key="text" className="text-blue-300 text-sm font-medium">
|
|||
|
|
Нажмите на версию для подробностей
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div key="roadmap-container" className="max-w-6xl mx-auto">
|
|||
|
|
<div key="timeline" className="relative">
|
|||
|
|
{/* Линия убрана */}
|
|||
|
|
|
|||
|
|
<div key="phases" className="space-y-8">
|
|||
|
|
{phases.map((phase, index) => {
|
|||
|
|
const statusConfig = getStatusConfig(phase.status);
|
|||
|
|
const isExpanded = selectedPhase === index;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div key={`phase-${index}`} className="relative">
|
|||
|
|
{/* Точки видны только на sm+ */}
|
|||
|
|
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
aria-expanded={isExpanded}
|
|||
|
|
onClick={() => togglePhaseDetail(index)}
|
|||
|
|
key={`phase-button-${index}`}
|
|||
|
|
className={`card-minimal rounded-xl p-4 text-left w-full transition-all duration-300 ${
|
|||
|
|
isExpanded
|
|||
|
|
? "ring-2 ring-" + statusConfig.color + "-500/30"
|
|||
|
|
: ""
|
|||
|
|
}`}
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
key="phase-header"
|
|||
|
|
className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-4 space-y-2 sm:space-y-0"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
key="phase-info"
|
|||
|
|
className="flex flex-col sm:flex-row sm:items-center sm:space-x-4"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
key="version-badge"
|
|||
|
|
className={`px-3 py-1 ${statusConfig.bgClass} border rounded-lg mb-2 sm:mb-0`}
|
|||
|
|
>
|
|||
|
|
<span
|
|||
|
|
key="version"
|
|||
|
|
className={`${statusConfig.textClass} font-bold text-sm`}
|
|||
|
|
>
|
|||
|
|
{phase.version}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div key="title-section">
|
|||
|
|
<h4
|
|||
|
|
key="title"
|
|||
|
|
className="text-lg font-semibold text-primary"
|
|||
|
|
>
|
|||
|
|
{phase.title}
|
|||
|
|
</h4>
|
|||
|
|
<p
|
|||
|
|
key="description"
|
|||
|
|
className="text-secondary text-sm"
|
|||
|
|
>
|
|||
|
|
{phase.description}
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div
|
|||
|
|
key="phase-meta"
|
|||
|
|
className="flex items-center space-x-3 text-sm text-gray-400 font-medium"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
key="status-badge"
|
|||
|
|
className={`flex items-center px-3 py-1 ${statusConfig.bgClass} border rounded-lg`}
|
|||
|
|
>
|
|||
|
|
<i
|
|||
|
|
key="status-icon"
|
|||
|
|
className={`${statusConfig.icon} ${statusConfig.textClass} mr-2 text-xs`}
|
|||
|
|
/>
|
|||
|
|
<span
|
|||
|
|
key="status-text"
|
|||
|
|
className={`${statusConfig.textClass} text-xs font-medium`}
|
|||
|
|
>
|
|||
|
|
{statusConfig.label}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div key="date">{phase.date}</div>
|
|||
|
|
<i
|
|||
|
|
key="expand-icon"
|
|||
|
|
className={`fas fa-chevron-${
|
|||
|
|
isExpanded ? "up" : "down"
|
|||
|
|
} text-gray-400 text-sm`}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{isExpanded && (
|
|||
|
|
<div
|
|||
|
|
key="features-section"
|
|||
|
|
className="mt-6 pt-6 border-t border-gray-700/30"
|
|||
|
|
>
|
|||
|
|
<h5
|
|||
|
|
key="features-title"
|
|||
|
|
className="text-primary font-medium mb-4 flex items-center"
|
|||
|
|
>
|
|||
|
|
<i
|
|||
|
|
key="features-icon"
|
|||
|
|
className="fas fa-list-ul mr-2 text-sm"
|
|||
|
|
/>
|
|||
|
|
Ключевые функции:
|
|||
|
|
</h5>
|
|||
|
|
|
|||
|
|
<div
|
|||
|
|
key="features-grid"
|
|||
|
|
className="grid md:grid-cols-2 gap-3"
|
|||
|
|
>
|
|||
|
|
{phase.features.map((feature, featureIndex) => (
|
|||
|
|
<div
|
|||
|
|
key={`feature-${featureIndex}`}
|
|||
|
|
className="flex items-center space-x-3 p-3 bg-custom-bg rounded-lg"
|
|||
|
|
>
|
|||
|
|
<div
|
|||
|
|
className={`w-2 h-2 rounded-full ${statusConfig.textClass.replace(
|
|||
|
|
"text-",
|
|||
|
|
"bg-"
|
|||
|
|
)}`}
|
|||
|
|
/>
|
|||
|
|
<span className="text-secondary text-sm">
|
|||
|
|
{feature}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div key="cta-section" className="mt-12 text-center">
|
|||
|
|
<div
|
|||
|
|
key="cta-card"
|
|||
|
|
className="card-minimal rounded-xl p-8 max-w-2xl mx-auto"
|
|||
|
|
>
|
|||
|
|
<h4
|
|||
|
|
key="cta-title"
|
|||
|
|
className="text-xl font-semibold text-primary mb-3"
|
|||
|
|
>
|
|||
|
|
Присоединяйтесь к будущему приватности
|
|||
|
|
</h4>
|
|||
|
|
<p key="cta-description" className="text-secondary mb-6">
|
|||
|
|
LockBit.chat развивается благодаря сообществу. Ваши идеи и feedback
|
|||
|
|
помогают формировать будущее защищенного общения.
|
|||
|
|
</p>
|
|||
|
|
|
|||
|
|
<div
|
|||
|
|
key="cta-buttons"
|
|||
|
|
className="flex flex-col sm:flex-row gap-4 justify-center"
|
|||
|
|
>
|
|||
|
|
<a
|
|||
|
|
key="github-link"
|
|||
|
|
href="https://github.com/lockbitchat/lockbit-chat/"
|
|||
|
|
className="btn-primary text-white py-3 px-6 rounded-lg font-medium transition-all duration-200 flex items-center justify-center"
|
|||
|
|
>
|
|||
|
|
<i key="github-icon" className="fab fa-github mr-2" />
|
|||
|
|
GitHub Repository
|
|||
|
|
</a>
|
|||
|
|
|
|||
|
|
<a
|
|||
|
|
key="feedback-link"
|
|||
|
|
href="mailto:lockbitchat@tutanota.com"
|
|||
|
|
className="btn-secondary text-white py-3 px-6 rounded-lg font-medium transition-all duration-200 flex items-center justify-center"
|
|||
|
|
>
|
|||
|
|
<i key="feedback-icon" className="fas fa-comments mr-2" />
|
|||
|
|
Обратная связь
|
|||
|
|
</a>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Enhanced Header Component with verification status
|
|||
|
|
const EnhancedMinimalHeader = ({ status, fingerprint, verificationCode, onDisconnect, isConnected, securityLevel, sessionManager, sessionTimeLeft }) => {
|
|||
|
|
const getStatusConfig = () => {
|
|||
|
|
switch (status) {
|
|||
|
|
case 'connected':
|
|||
|
|
return {
|
|||
|
|
text: 'Подключено',
|
|||
|
|
className: 'status-connected',
|
|||
|
|
badgeClass: 'bg-green-500/10 text-green-400 border-green-500/20'
|
|||
|
|
};
|
|||
|
|
case 'verifying':
|
|||
|
|
return {
|
|||
|
|
text: 'Верификация...',
|
|||
|
|
className: 'status-verifying',
|
|||
|
|
badgeClass: 'bg-purple-500/10 text-purple-400 border-purple-500/20'
|
|||
|
|
};
|
|||
|
|
case 'connecting':
|
|||
|
|
return {
|
|||
|
|
text: 'Подключение...',
|
|||
|
|
className: 'status-connecting',
|
|||
|
|
badgeClass: 'bg-blue-500/10 text-blue-400 border-blue-500/20'
|
|||
|
|
};
|
|||
|
|
case 'retrying':
|
|||
|
|
return {
|
|||
|
|
text: 'Переподключение...',
|
|||
|
|
className: 'status-connecting',
|
|||
|
|
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
|
|||
|
|
};
|
|||
|
|
case 'failed':
|
|||
|
|
return {
|
|||
|
|
text: 'Ошибка',
|
|||
|
|
className: 'status-failed',
|
|||
|
|
badgeClass: 'bg-red-500/10 text-red-400 border-red-500/20'
|
|||
|
|
};
|
|||
|
|
case 'reconnecting':
|
|||
|
|
return {
|
|||
|
|
text: 'Переподключение...',
|
|||
|
|
className: 'status-connecting',
|
|||
|
|
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
|
|||
|
|
};
|
|||
|
|
case 'peer_disconnected':
|
|||
|
|
return {
|
|||
|
|
text: 'Собеседник отключился',
|
|||
|
|
className: 'status-failed',
|
|||
|
|
badgeClass: 'bg-orange-500/10 text-orange-400 border-orange-500/20'
|
|||
|
|
};
|
|||
|
|
default:
|
|||
|
|
return {
|
|||
|
|
text: 'Не подключен',
|
|||
|
|
className: 'status-disconnected',
|
|||
|
|
badgeClass: 'bg-gray-500/10 text-gray-400 border-gray-500/20'
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const config = getStatusConfig();
|
|||
|
|
|
|||
|
|
return React.createElement('header', {
|
|||
|
|
className: 'header-minimal sticky top-0 z-50'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'container',
|
|||
|
|
className: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'content',
|
|||
|
|
className: 'flex items-center justify-between h-16'
|
|||
|
|
}, [
|
|||
|
|
// Logo and Title - Mobile Responsive
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'logo-section',
|
|||
|
|
className: 'flex items-center space-x-2 sm:space-x-3'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'logo',
|
|||
|
|
className: 'icon-container w-8 h-8 sm:w-10 sm:h-10'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-shield-halved accent-orange text-sm sm:text-base'
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'title-section'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('h1', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: 'text-lg sm:text-xl font-semibold text-primary'
|
|||
|
|
}, 'LockBit.chat'),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'subtitle',
|
|||
|
|
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
|||
|
|
}, 'End-to-end freedom')
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Status and Controls - Mobile Responsive
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'status-section',
|
|||
|
|
className: 'flex items-center space-x-2 sm:space-x-3'
|
|||
|
|
}, [
|
|||
|
|
// Session Timer - показывать если есть активная сессия
|
|||
|
|
sessionManager?.hasActiveSession() && React.createElement(SessionTimer, {
|
|||
|
|
key: 'session-timer',
|
|||
|
|
timeLeft: sessionTimeLeft,
|
|||
|
|
sessionType: sessionManager.currentSession?.type || 'unknown'
|
|||
|
|
}),
|
|||
|
|
// Security Level Indicator - Hidden on mobile, shown on tablet+ (Clickable)
|
|||
|
|
securityLevel && React.createElement('div', {
|
|||
|
|
key: 'security-level',
|
|||
|
|
className: 'hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200',
|
|||
|
|
onClick: () => {
|
|||
|
|
if (securityLevel.verificationResults) {
|
|||
|
|
console.log('Security verification results:', securityLevel.verificationResults);
|
|||
|
|
alert('Детали проверки безопасности:\n\n' +
|
|||
|
|
Object.entries(securityLevel.verificationResults)
|
|||
|
|
.map(([key, result]) => `${key}: ${result.passed ? '✅' : '❌'} ${result.details}`)
|
|||
|
|
.join('\n')
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
title: 'Нажмите для просмотра деталей безопасности'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'security-icon',
|
|||
|
|
className: `w-6 h-6 rounded-full flex items-center justify-center ${
|
|||
|
|
securityLevel.color === 'green' ? 'bg-green-500/20' :
|
|||
|
|
securityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
|||
|
|
}`
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: `fas fa-shield-alt text-xs ${
|
|||
|
|
securityLevel.color === 'green' ? 'text-green-400' :
|
|||
|
|
securityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
|
|||
|
|
}`
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'security-info',
|
|||
|
|
className: 'flex flex-col'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'security-level-text',
|
|||
|
|
className: 'text-xs font-medium text-primary'
|
|||
|
|
}, `${securityLevel.level} (${securityLevel.score}%)`),
|
|||
|
|
securityLevel.details && React.createElement('div', {
|
|||
|
|
key: 'security-details',
|
|||
|
|
className: 'text-xs text-muted mt-1 hidden lg:block'
|
|||
|
|
}, securityLevel.details),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'security-progress',
|
|||
|
|
className: 'w-16 h-1 bg-gray-600 rounded-full overflow-hidden'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'progress-bar',
|
|||
|
|
className: `h-full transition-all duration-500 ${
|
|||
|
|
securityLevel.color === 'green' ? 'bg-green-400' :
|
|||
|
|
securityLevel.color === 'yellow' ? 'bg-yellow-400' : 'bg-red-400'
|
|||
|
|
}`,
|
|||
|
|
style: { width: `${securityLevel.score}%` }
|
|||
|
|
})
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Mobile Security Indicator - Only icon on mobile (Clickable)
|
|||
|
|
securityLevel && React.createElement('div', {
|
|||
|
|
key: 'mobile-security',
|
|||
|
|
className: 'md:hidden flex items-center'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'mobile-security-icon',
|
|||
|
|
className: `w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity duration-200 ${
|
|||
|
|
securityLevel.color === 'green' ? 'bg-green-500/20' :
|
|||
|
|
securityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
|||
|
|
}`,
|
|||
|
|
title: `${securityLevel.level} (${securityLevel.score}%) - Нажмите для деталей`,
|
|||
|
|
onClick: () => {
|
|||
|
|
if (securityLevel.verificationResults) {
|
|||
|
|
console.log('Security verification results:', securityLevel.verificationResults);
|
|||
|
|
alert('Детали проверки безопасности:\n\n' +
|
|||
|
|
Object.entries(securityLevel.verificationResults)
|
|||
|
|
.map(([key, result]) => `${key}: ${result.passed ? '✅' : '❌'} ${result.details}`)
|
|||
|
|
.join('\n')
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: `fas fa-shield-alt text-sm ${
|
|||
|
|
securityLevel.color === 'green' ? 'text-green-400' :
|
|||
|
|
securityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
|
|||
|
|
}`
|
|||
|
|
})
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Status Badge - Compact on mobile
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'status-badge',
|
|||
|
|
className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2`
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'status-dot',
|
|||
|
|
className: `status-dot ${config.className}`
|
|||
|
|
}),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'status-text',
|
|||
|
|
className: 'text-xs sm:text-sm font-medium'
|
|||
|
|
}, config.text)
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Disconnect Button - Icon only on mobile
|
|||
|
|
isConnected && React.createElement('button', {
|
|||
|
|
key: 'disconnect-btn',
|
|||
|
|
onClick: onDisconnect,
|
|||
|
|
className: 'p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm'
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
key: 'disconnect-icon',
|
|||
|
|
className: 'fas fa-power-off sm:mr-2'
|
|||
|
|
}),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'disconnect-text',
|
|||
|
|
className: 'hidden sm:inline'
|
|||
|
|
}, 'Отключить')
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
|
|||
|
|
]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Enhanced Copy Button with better UX
|
|||
|
|
const EnhancedCopyButton = ({ text, className = "", children }) => {
|
|||
|
|
const [copied, setCopied] = React.useState(false);
|
|||
|
|
|
|||
|
|
const handleCopy = async () => {
|
|||
|
|
try {
|
|||
|
|
await navigator.clipboard.writeText(text);
|
|||
|
|
setCopied(true);
|
|||
|
|
setTimeout(() => setCopied(false), 2000);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Copy failed:', error);
|
|||
|
|
// Fallback for older browsers
|
|||
|
|
const textArea = document.createElement('textarea');
|
|||
|
|
textArea.value = text;
|
|||
|
|
document.body.appendChild(textArea);
|
|||
|
|
textArea.select();
|
|||
|
|
document.execCommand('copy');
|
|||
|
|
document.body.removeChild(textArea);
|
|||
|
|
setCopied(true);
|
|||
|
|
setTimeout(() => setCopied(false), 2000);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return React.createElement('button', {
|
|||
|
|
onClick: handleCopy,
|
|||
|
|
className: `${className} transition-all duration-200`
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: `${copied ? 'fas fa-check accent-green' : 'fas fa-copy text-secondary'} mr-2`
|
|||
|
|
}),
|
|||
|
|
copied ? 'Скопировано!' : children
|
|||
|
|
]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Verification Component
|
|||
|
|
const VerificationStep = ({ verificationCode, onConfirm, onReject }) => {
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
className: "card-minimal rounded-xl p-6 border-purple-500/20"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'header',
|
|||
|
|
className: "flex items-center mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: "w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-shield-alt accent-purple'
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-medium text-primary"
|
|||
|
|
}, "Верификация безопасности")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'content',
|
|||
|
|
className: "space-y-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'description',
|
|||
|
|
className: "text-secondary text-sm"
|
|||
|
|
}, "Сверьте код безопасности с собеседником по другому каналу связи (голос, SMS, etc):"),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'code-display',
|
|||
|
|
className: "text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'code',
|
|||
|
|
className: "verification-code text-2xl py-4"
|
|||
|
|
}, verificationCode)
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'warning',
|
|||
|
|
className: "p-3 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('p', {
|
|||
|
|
className: "text-yellow-400 text-sm flex items-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-exclamation-triangle mr-2'
|
|||
|
|
}),
|
|||
|
|
'Убедитесь, что коды полностью совпадают!'
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'buttons',
|
|||
|
|
className: "flex space-x-3"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('button', {
|
|||
|
|
key: 'confirm',
|
|||
|
|
onClick: onConfirm,
|
|||
|
|
className: "flex-1 btn-verify text-white py-3 px-4 rounded-lg font-medium transition-all duration-200"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-check mr-2'
|
|||
|
|
}),
|
|||
|
|
'Коды совпадают'
|
|||
|
|
]),
|
|||
|
|
React.createElement('button', {
|
|||
|
|
key: 'reject',
|
|||
|
|
onClick: onReject,
|
|||
|
|
className: "flex-1 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 py-3 px-4 rounded-lg font-medium transition-all duration-200"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-times mr-2'
|
|||
|
|
}),
|
|||
|
|
'Коды не совпадают'
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Enhanced Chat Message with better security indicators
|
|||
|
|
const EnhancedChatMessage = ({ message, type, timestamp }) => {
|
|||
|
|
const formatTime = (ts) => {
|
|||
|
|
return new Date(ts).toLocaleTimeString('ru-RU', {
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit',
|
|||
|
|
second: '2-digit'
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const getMessageStyle = () => {
|
|||
|
|
switch (type) {
|
|||
|
|
case 'sent':
|
|||
|
|
return {
|
|||
|
|
container: "ml-auto bg-orange-500/15 border-orange-500/20 text-primary",
|
|||
|
|
icon: "fas fa-lock accent-orange",
|
|||
|
|
label: "Зашифровано"
|
|||
|
|
};
|
|||
|
|
case 'received':
|
|||
|
|
return {
|
|||
|
|
container: "mr-auto card-minimal text-primary",
|
|||
|
|
icon: "fas fa-unlock-alt accent-green",
|
|||
|
|
label: "Расшифровано"
|
|||
|
|
};
|
|||
|
|
case 'system':
|
|||
|
|
return {
|
|||
|
|
container: "mx-auto bg-yellow-500/10 border border-yellow-500/20 text-yellow-400",
|
|||
|
|
icon: "fas fa-info-circle accent-yellow",
|
|||
|
|
label: "Система"
|
|||
|
|
};
|
|||
|
|
default:
|
|||
|
|
return {
|
|||
|
|
container: "mx-auto card-minimal text-secondary",
|
|||
|
|
icon: "fas fa-circle text-muted",
|
|||
|
|
label: "Неизвестно"
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const style = getMessageStyle();
|
|||
|
|
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
className: `message-slide mb-3 p-3 rounded-lg max-w-md break-words ${style.container} border`
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'content',
|
|||
|
|
className: "flex items-start space-x-2"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: `${style.icon} text-sm mt-0.5 opacity-70`
|
|||
|
|
}),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'text',
|
|||
|
|
className: "flex-1"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'message',
|
|||
|
|
className: "text-sm"
|
|||
|
|
}, message),
|
|||
|
|
timestamp && React.createElement('div', {
|
|||
|
|
key: 'meta',
|
|||
|
|
className: "flex items-center justify-between mt-1 text-xs opacity-50"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'time'
|
|||
|
|
}, formatTime(timestamp)),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'status',
|
|||
|
|
className: "text-xs"
|
|||
|
|
}, style.label)
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Enhanced Connection Setup with verification
|
|||
|
|
const EnhancedConnectionSetup = ({
|
|||
|
|
messages,
|
|||
|
|
onCreateOffer,
|
|||
|
|
onCreateAnswer,
|
|||
|
|
onConnect,
|
|||
|
|
onClearData,
|
|||
|
|
onVerifyConnection,
|
|||
|
|
connectionStatus,
|
|||
|
|
offerData,
|
|||
|
|
answerData,
|
|||
|
|
offerInput,
|
|||
|
|
setOfferInput,
|
|||
|
|
answerInput,
|
|||
|
|
setAnswerInput,
|
|||
|
|
showOfferStep,
|
|||
|
|
showAnswerStep,
|
|||
|
|
verificationCode,
|
|||
|
|
showVerification,
|
|||
|
|
offerPassword,
|
|||
|
|
answerPassword
|
|||
|
|
}) => {
|
|||
|
|
const [mode, setMode] = React.useState('select');
|
|||
|
|
|
|||
|
|
const resetToSelect = () => {
|
|||
|
|
setMode('select');
|
|||
|
|
onClearData();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleVerificationConfirm = () => {
|
|||
|
|
onVerifyConnection(true);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleVerificationReject = () => {
|
|||
|
|
onVerifyConnection(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
if (showVerification) {
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'verification',
|
|||
|
|
className: "w-full max-w-md"
|
|||
|
|
}, [
|
|||
|
|
React.createElement(VerificationStep, {
|
|||
|
|
verificationCode: verificationCode,
|
|||
|
|
onConfirm: handleVerificationConfirm,
|
|||
|
|
onReject: handleVerificationReject
|
|||
|
|
})
|
|||
|
|
])
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (mode === 'select') {
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'selector',
|
|||
|
|
className: "w-full max-w-4xl"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'header',
|
|||
|
|
className: "text-center mb-8"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('h2', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-2xl font-semibold text-primary mb-3"
|
|||
|
|
}, 'Начать защищенное общение'),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'subtitle',
|
|||
|
|
className: "text-secondary max-w-2xl mx-auto"
|
|||
|
|
}, "Выберите способ подключения к защищенному каналу связи с ECDH шифрованием и Perfect Forward Secrecy")
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'options',
|
|||
|
|
className: "grid md:grid-cols-2 gap-6 max-w-3xl mx-auto"
|
|||
|
|
}, [
|
|||
|
|
// Create Connection
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'create',
|
|||
|
|
onClick: () => setMode('create'),
|
|||
|
|
className: "card-minimal rounded-xl p-6 cursor-pointer group"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: "w-12 h-12 bg-blue-500/10 border border-blue-500/20 rounded-lg flex items-center justify-center mx-auto mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-plus text-xl text-blue-400'
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-semibold text-primary text-center mb-3"
|
|||
|
|
}, "Создать канал"),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'description',
|
|||
|
|
className: "text-secondary text-center text-sm mb-4"
|
|||
|
|
}, "Инициируйте новое защищенное соединение с шифрованным обменом"),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'features',
|
|||
|
|
className: "space-y-2"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f1',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-key accent-orange mr-2 text-xs'
|
|||
|
|
}),
|
|||
|
|
'Генерация ECDH ключей'
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f2',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-shield-alt accent-orange mr-2 text-xs'
|
|||
|
|
}),
|
|||
|
|
'Код верификации'
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f3',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-sync-alt accent-purple mr-2 text-xs'
|
|||
|
|
}),
|
|||
|
|
'PFS ротация ключей'
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Join Connection
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'join',
|
|||
|
|
onClick: () => setMode('join'),
|
|||
|
|
className: "card-minimal rounded-xl p-6 cursor-pointer group"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: "w-12 h-12 bg-green-500/10 border border-green-500/20 rounded-lg flex items-center justify-center mx-auto mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-link text-xl accent-green'
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-semibold text-primary text-center mb-3"
|
|||
|
|
}, "Присоединиться"),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'description',
|
|||
|
|
className: "text-secondary text-center text-sm mb-4"
|
|||
|
|
}, "Подключитесь к существующему защищенному каналу"),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'features',
|
|||
|
|
className: "space-y-2"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f1',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-paste accent-green mr-2 text-xs'
|
|||
|
|
}),
|
|||
|
|
'Вставка Offer приглашения'
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f2',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-check-circle accent-green mr-2 text-xs'
|
|||
|
|
}),
|
|||
|
|
'Автоматическая верификация'
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f3',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-sync-alt accent-purple mr-2 text-xs'
|
|||
|
|
}),
|
|||
|
|
'PFS защита'
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'security-features',
|
|||
|
|
className: "grid grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 max-w-6xl mx-auto mt-8"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', { key: 'feature1', className: "text-center p-3 sm:p-4" }, [
|
|||
|
|
React.createElement('div', { key: 'icon', className: "w-10 h-10 sm:w-12 sm:h-12 bg-green-500/10 border border-green-500/20 rounded-lg flex items-center justify-center mx-auto mb-2 sm:mb-3" }, [
|
|||
|
|
React.createElement('i', { className: 'fas fa-exchange-alt accent-green' })
|
|||
|
|
]),
|
|||
|
|
React.createElement('h4', { key: 'title', className: "text-xs sm:text-sm font-medium text-primary mb-1" }, "ECDH Key Exchange"),
|
|||
|
|
React.createElement('p', { key: 'desc', className: "text-xs text-muted leading-tight" }, "Безопасный обмен ключами")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', { key: 'feature2', className: "text-center p-3 sm:p-4" }, [
|
|||
|
|
React.createElement('div', { key: 'icon', className: "w-10 h-10 sm:w-12 sm:h-12 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mx-auto mb-2 sm:mb-3" }, [
|
|||
|
|
React.createElement('i', { className: 'fas fa-shield-alt accent-purple' })
|
|||
|
|
]),
|
|||
|
|
React.createElement('h4', { key: 'title', className: "text-xs sm:text-sm font-medium text-primary mb-1" }, "Out-of-Band Verification"),
|
|||
|
|
React.createElement('p', { key: 'desc', className: "text-xs text-muted leading-tight" }, "Защита от MITM атак")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', { key: 'feature3', className: "text-center p-3 sm:p-4" }, [
|
|||
|
|
React.createElement('div', { key: 'icon', className: "w-10 h-10 sm:w-12 sm:h-12 bg-orange-500/10 border border-orange-500/20 rounded-lg flex items-center justify-center mx-auto mb-2 sm:mb-3" }, [
|
|||
|
|
React.createElement('i', { className: 'fas fa-lock accent-orange' })
|
|||
|
|
]),
|
|||
|
|
React.createElement('h4', { key: 'title', className: "text-xs sm:text-sm font-medium text-primary mb-1" }, "Шифрование"),
|
|||
|
|
React.createElement('p', { key: 'desc', className: "text-xs text-muted leading-tight" }, "Шифрование + аутентификация")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', { key: 'feature4', className: "text-center p-3 sm:p-4" }, [
|
|||
|
|
React.createElement('div', { key: 'icon', className: "w-10 h-10 sm:w-12 sm:h-12 bg-cyan-500/10 border border-cyan-500/20 rounded-lg flex items-center justify-center mx-auto mb-2 sm:mb-3" }, [
|
|||
|
|
React.createElement('i', { className: 'fas fa-sync-alt accent-cyan' })
|
|||
|
|
]),
|
|||
|
|
React.createElement('h4', { key: 'title', className: "text-xs sm:text-sm font-medium text-primary mb-1" }, "Perfect Forward Secrecy"),
|
|||
|
|
React.createElement('p', { key: 'desc', className: "text-xs text-muted leading-tight" }, "Автоматическая ротация ключей")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', { key: 'feature5', className: "text-center p-3 sm:p-4" }, [
|
|||
|
|
React.createElement('div', { key: 'icon', className: "w-10 h-10 sm:w-12 sm:h-12 bg-blue-500/10 border border-blue-500/20 rounded-lg flex items-center justify-center mx-auto mb-2 sm:mb-3" }, [
|
|||
|
|
React.createElement('i', { className: 'fas fa-fingerprint accent-blue' })
|
|||
|
|
]),
|
|||
|
|
React.createElement('h4', { key: 'title', className: "text-xs sm:text-sm font-medium text-primary mb-1" }, "Цифровые подписи"),
|
|||
|
|
React.createElement('p', { key: 'desc', className: "text-xs text-muted leading-tight" }, "ECDSA верификация сообщений")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', { key: 'feature6', className: "text-center p-3 sm:p-4" }, [
|
|||
|
|
React.createElement('div', { key: 'icon', className: "w-10 h-10 sm:w-12 sm:h-12 bg-red-500/10 border border-red-500/20 rounded-lg flex items-center justify-center mx-auto mb-2 sm:mb-3" }, [
|
|||
|
|
React.createElement('i', { className: 'fas fa-ban accent-red' })
|
|||
|
|
]),
|
|||
|
|
React.createElement('h4', { key: 'title', className: "text-xs sm:text-sm font-medium text-primary mb-1" }, "Защита от перехвата"),
|
|||
|
|
React.createElement('p', { key: 'desc', className: "text-xs text-muted leading-tight" }, "Шифрованный обмен данными")
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Wallet Logos Section
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'wallet-logos-section',
|
|||
|
|
className: "mt-8"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'wallet-logos-header',
|
|||
|
|
className: "text-center mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-medium text-primary mb-2"
|
|||
|
|
}, "Поддерживаемые Lightning кошельки"),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'subtitle',
|
|||
|
|
className: "text-secondary text-sm"
|
|||
|
|
}, "Для оплаты сессий используйте любой из популярных кошельков")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'wallet-logos-container',
|
|||
|
|
className: "wallet-logos-container"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'wallet-logos-track',
|
|||
|
|
className: "wallet-logos-track"
|
|||
|
|
}, [
|
|||
|
|
// First set of logos
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'alby1-link',
|
|||
|
|
href: "https://getalby.com",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo alby"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'alby-img1',
|
|||
|
|
src: "logo/alby.svg",
|
|||
|
|
alt: "Alby Lightning Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'zeus1-link',
|
|||
|
|
href: "https://zeusln.app",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo zeus"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'zeus-img1',
|
|||
|
|
src: "logo/zeus.svg",
|
|||
|
|
alt: "Zeus Lightning Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'wos1-link',
|
|||
|
|
href: "https://www.walletofsatoshi.com",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo wos"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'wos-img1',
|
|||
|
|
src: "logo/wos.svg",
|
|||
|
|
alt: "Wallet of Satoshi",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'muun1-link',
|
|||
|
|
href: "https://muun.com",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo muun"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'muun-img1',
|
|||
|
|
src: "logo/muun.svg",
|
|||
|
|
alt: "Muun Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'atomic1-link',
|
|||
|
|
href: "https://atomicwallet.io",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo atomic"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'atomic-img1',
|
|||
|
|
src: "logo/atomic.svg",
|
|||
|
|
alt: "Atomic Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'breez1-link',
|
|||
|
|
href: "https://breez.technology/mobile/",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo breez"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'breez-img1',
|
|||
|
|
src: "logo/breez.svg",
|
|||
|
|
alt: "Breez Lightning Wallet",
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'lightning-labs1-link',
|
|||
|
|
href: "https://lightning.engineering",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo lightning-labs"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'lightning-labs-img1',
|
|||
|
|
src: "logo/lightning-labs.svg",
|
|||
|
|
alt: "Lightning Labs",
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'lnbits1-link',
|
|||
|
|
href: "https://lnbits.com",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo lnbits"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'lnbits-img1',
|
|||
|
|
src: "logo/lnbits.svg",
|
|||
|
|
alt: "LNbits",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'strike1-link',
|
|||
|
|
href: "https://strike.me",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo strike"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'strike-img1',
|
|||
|
|
src: "logo/strike.svg",
|
|||
|
|
alt: "Strike",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'impervious1-link',
|
|||
|
|
href: "https://impervious.ai",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo impervious"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'impervious-img1',
|
|||
|
|
src: "logo/impervious.svg",
|
|||
|
|
alt: "Impervious",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'bitcoin-lightning1-link',
|
|||
|
|
href: "https://www.blink.sv/",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo bitcoin-lightning"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'blink-img1',
|
|||
|
|
src: "logo/blink.svg",
|
|||
|
|
alt: "Blink Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
// Second set of logos
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'alby2-link',
|
|||
|
|
href: "https://getalby.com",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo alby"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'alby-img2',
|
|||
|
|
src: "logo/alby.svg",
|
|||
|
|
alt: "Alby Lightning Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'zeus2-link',
|
|||
|
|
href: "https://zeusln.app",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo zeus"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'zeus-img2',
|
|||
|
|
src: "logo/zeus.svg",
|
|||
|
|
alt: "Zeus Lightning Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'wos2-link',
|
|||
|
|
href: "https://www.walletofsatoshi.com",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo wos"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'wos-img2',
|
|||
|
|
src: "logo/wos.svg",
|
|||
|
|
alt: "Wallet of Satoshi",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'muun2-link',
|
|||
|
|
href: "https://muun.com",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo muun"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'muun-img2',
|
|||
|
|
src: "logo/muun.svg",
|
|||
|
|
alt: "Muun Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'atomic2-link',
|
|||
|
|
href: "https://atomicwallet.io",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo atomic"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'atomic-img2',
|
|||
|
|
src: "logo/atomic.svg",
|
|||
|
|
alt: "Atomic Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'breez2-link',
|
|||
|
|
href: "https://breez.technology/mobile/",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo breez"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'breez-img2',
|
|||
|
|
src: "logo/breez.svg",
|
|||
|
|
alt: "Breez Lightning Wallet",
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'lightning-labs2-link',
|
|||
|
|
href: "https://lightning.engineering",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo lightning-labs"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'lightning-labs-img2',
|
|||
|
|
src: "logo/lightning-labs.svg",
|
|||
|
|
alt: "Lightning Labs",
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'lnbits2-link',
|
|||
|
|
href: "https://lnbits.com",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo lnbits"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'lnbits-img2',
|
|||
|
|
src: "logo/lnbits.svg",
|
|||
|
|
alt: "LNbits",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'strike2-link',
|
|||
|
|
href: "https://strike.me",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo strike"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'strike-img2',
|
|||
|
|
src: "logo/strike.svg",
|
|||
|
|
alt: "Strike",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'impervious2-link',
|
|||
|
|
href: "https://impervious.ai",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo impervious"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'impervious-img2',
|
|||
|
|
src: "logo/impervious.svg",
|
|||
|
|
alt: "Impervious",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('a', {
|
|||
|
|
key: 'bitcoin-lightning2-link',
|
|||
|
|
href: "https://www.blink.sv/",
|
|||
|
|
target: "_blank",
|
|||
|
|
rel: "noindex nofollow",
|
|||
|
|
className: "wallet-logo bitcoin-lightning"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('img', {
|
|||
|
|
key: 'blink-img2',
|
|||
|
|
src: "logo/blink.svg",
|
|||
|
|
alt: "Blink Wallet",
|
|||
|
|
className: "wallet-logo-img"
|
|||
|
|
})
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
React.createElement(UniqueFeatureSlider, { key: 'unique-features-slider' }),
|
|||
|
|
|
|||
|
|
React.createElement(ComparisonTable, { key: 'comparison-table' }),
|
|||
|
|
|
|||
|
|
React.createElement(Roadmap, { key: 'roadmap' }),
|
|||
|
|
])
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (mode === 'create') {
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'create-flow',
|
|||
|
|
className: "w-full max-w-3xl space-y-6"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'header',
|
|||
|
|
className: "text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('button', {
|
|||
|
|
key: 'back',
|
|||
|
|
onClick: resetToSelect,
|
|||
|
|
className: "mb-4 text-secondary hover:text-primary transition-colors flex items-center mx-auto text-sm"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-arrow-left mr-2'
|
|||
|
|
}),
|
|||
|
|
'Назад к выбору'
|
|||
|
|
]),
|
|||
|
|
React.createElement('h2', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-xl font-semibold text-primary mb-2"
|
|||
|
|
}, 'Создание защищенного канала')
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Step 1
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'step1',
|
|||
|
|
className: "card-minimal rounded-xl p-6"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'step-header',
|
|||
|
|
className: "flex items-center mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'number',
|
|||
|
|
className: "step-number mr-3"
|
|||
|
|
}, '1'),
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-medium text-primary"
|
|||
|
|
}, "Генерация ECDH ключей и кода верификации")
|
|||
|
|
]),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'description',
|
|||
|
|
className: "text-secondary text-sm mb-4"
|
|||
|
|
}, "Создание криптографически стойких ключей и кода для защиты от атак"),
|
|||
|
|
React.createElement('button', {
|
|||
|
|
key: 'create-btn',
|
|||
|
|
onClick: onCreateOffer,
|
|||
|
|
disabled: connectionStatus === 'connecting' || showOfferStep,
|
|||
|
|
className: `w-full btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed`
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-shield-alt mr-2'
|
|||
|
|
}),
|
|||
|
|
showOfferStep ? 'Ключи созданы ✓' : 'Создать защищенные ключи'
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
showOfferStep && React.createElement('div', {
|
|||
|
|
key: 'offer-result',
|
|||
|
|
className: "mt-6 space-y-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'success',
|
|||
|
|
className: "p-3 bg-green-500/10 border border-green-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('p', {
|
|||
|
|
className: "text-green-400 text-sm font-medium flex items-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-check-circle mr-2'
|
|||
|
|
}),
|
|||
|
|
'Зашифрованное приглашение создано! Отправьте код и пароль собеседнику:'
|
|||
|
|
]),
|
|||
|
|
offerPassword && React.createElement('div', {
|
|||
|
|
key: 'password-display',
|
|||
|
|
className: "mt-3 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'password-label',
|
|||
|
|
className: "text-blue-400 text-sm font-medium mb-2"
|
|||
|
|
}, '🔑 Пароль для расшифровки:'),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'password-container',
|
|||
|
|
className: "flex items-center space-x-2"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('code', {
|
|||
|
|
key: 'password',
|
|||
|
|
className: "flex-1 p-2 bg-gray-900/50 border border-gray-500/30 rounded font-mono text-sm text-blue-300 font-medium"
|
|||
|
|
}, offerPassword),
|
|||
|
|
React.createElement(EnhancedCopyButton, {
|
|||
|
|
key: 'copy-password',
|
|||
|
|
text: offerPassword,
|
|||
|
|
className: "px-3 py-2 bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 border border-blue-500/30 rounded text-sm"
|
|||
|
|
}, 'Копировать')
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'offer-data',
|
|||
|
|
className: "space-y-3"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('textarea', {
|
|||
|
|
key: 'textarea',
|
|||
|
|
value: offerData,
|
|||
|
|
readOnly: true,
|
|||
|
|
rows: 8,
|
|||
|
|
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
|
|||
|
|
}),
|
|||
|
|
React.createElement(EnhancedCopyButton, {
|
|||
|
|
key: 'copy',
|
|||
|
|
text: offerData,
|
|||
|
|
className: "w-full px-3 py-2 bg-orange-500/10 hover:bg-orange-500/20 text-orange-400 border border-orange-500/20 rounded text-sm font-medium"
|
|||
|
|
}, 'Копировать зашифрованный код')
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Step 2
|
|||
|
|
showOfferStep && React.createElement('div', {
|
|||
|
|
key: 'step2',
|
|||
|
|
className: "card-minimal rounded-xl p-6"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'step-header',
|
|||
|
|
className: "flex items-center mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'number',
|
|||
|
|
className: "w-8 h-8 bg-green-500 text-white rounded-lg flex items-center justify-center font-semibold text-sm mr-3"
|
|||
|
|
}, '2'),
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-medium text-primary"
|
|||
|
|
}, "Ожидание ответа собеседника")
|
|||
|
|
]),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'description',
|
|||
|
|
className: "text-secondary text-sm mb-4"
|
|||
|
|
}, "Вставьте зашифрованный код приглашения от собеседника"),
|
|||
|
|
React.createElement('textarea', {
|
|||
|
|
key: 'input',
|
|||
|
|
value: answerInput,
|
|||
|
|
onChange: (e) => setAnswerInput(e.target.value),
|
|||
|
|
rows: 6,
|
|||
|
|
placeholder: "Вставьте зашифрованный код ответа от собеседника...",
|
|||
|
|
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg resize-none mb-4 text-secondary placeholder-gray-500 focus:border-orange-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
|||
|
|
}),
|
|||
|
|
React.createElement('button', {
|
|||
|
|
key: 'connect-btn',
|
|||
|
|
onClick: onConnect,
|
|||
|
|
disabled: !answerInput.trim(),
|
|||
|
|
className: "w-full btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-rocket mr-2'
|
|||
|
|
}),
|
|||
|
|
'Установить соединение'
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (mode === 'join') {
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
className: "min-h-[calc(100vh-104px)] flex items-center justify-center p-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'join-flow',
|
|||
|
|
className: "w-full max-w-3xl space-y-6"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'header',
|
|||
|
|
className: "text-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('button', {
|
|||
|
|
key: 'back',
|
|||
|
|
onClick: resetToSelect,
|
|||
|
|
className: "mb-4 text-secondary hover:text-primary transition-colors flex items-center mx-auto text-sm"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-arrow-left mr-2'
|
|||
|
|
}),
|
|||
|
|
'Назад к выбору'
|
|||
|
|
]),
|
|||
|
|
React.createElement('h2', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-xl font-semibold text-primary mb-2"
|
|||
|
|
}, 'Присоединение к защищенному каналу')
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Step 1
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'step1',
|
|||
|
|
className: "card-minimal rounded-xl p-6"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'step-header',
|
|||
|
|
className: "flex items-center mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'number',
|
|||
|
|
className: "w-8 h-8 bg-green-500 text-white rounded-lg flex items-center justify-center font-semibold text-sm mr-3"
|
|||
|
|
}, '1'),
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-medium text-primary"
|
|||
|
|
}, "Вставка защищенного приглашения")
|
|||
|
|
]),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'description',
|
|||
|
|
className: "text-secondary text-sm mb-4"
|
|||
|
|
}, "Скопируйте и вставьте зашифрованный код приглашения от инициатора"),
|
|||
|
|
React.createElement('textarea', {
|
|||
|
|
key: 'input',
|
|||
|
|
value: offerInput,
|
|||
|
|
onChange: (e) => setOfferInput(e.target.value),
|
|||
|
|
rows: 8,
|
|||
|
|
placeholder: "Вставьте зашифрованный код приглашения...",
|
|||
|
|
className: "w-full p-3 bg-custom-bg border border-gray-500/20 rounded-lg resize-none mb-4 text-secondary placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
|||
|
|
}),
|
|||
|
|
React.createElement('button', {
|
|||
|
|
key: 'process-btn',
|
|||
|
|
onClick: onCreateAnswer,
|
|||
|
|
disabled: !offerInput.trim() || connectionStatus === 'connecting',
|
|||
|
|
className: "w-full btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-cogs mr-2'
|
|||
|
|
}),
|
|||
|
|
'Обработать приглашение'
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Step 2
|
|||
|
|
showAnswerStep && React.createElement('div', {
|
|||
|
|
key: 'step2',
|
|||
|
|
className: "card-minimal rounded-xl p-6"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'step-header',
|
|||
|
|
className: "flex items-center mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'number',
|
|||
|
|
className: "step-number mr-3"
|
|||
|
|
}, '2'),
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-medium text-primary"
|
|||
|
|
}, "Отправка защищенного ответа")
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'success',
|
|||
|
|
className: "p-3 bg-green-500/10 border border-green-500/20 rounded-lg mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('p', {
|
|||
|
|
className: "text-green-400 text-sm font-medium flex items-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-check-circle mr-2'
|
|||
|
|
}),
|
|||
|
|
'Зашифрованный ответ создан! Отправьте этот код инициатору:'
|
|||
|
|
]),
|
|||
|
|
answerPassword && React.createElement('div', {
|
|||
|
|
key: 'password-display',
|
|||
|
|
className: "mt-3 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'password-label',
|
|||
|
|
className: "text-blue-400 text-sm font-medium mb-2"
|
|||
|
|
}, '🔑 Пароль для расшифровки:'),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'password-container',
|
|||
|
|
className: "flex items-center space-x-2"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('code', {
|
|||
|
|
key: 'password',
|
|||
|
|
className: "flex-1 p-2 bg-gray-900/50 border border-gray-500/30 rounded font-mono text-sm text-blue-300 font-medium"
|
|||
|
|
}, answerPassword),
|
|||
|
|
React.createElement(EnhancedCopyButton, {
|
|||
|
|
key: 'copy-password',
|
|||
|
|
text: answerPassword,
|
|||
|
|
className: "px-3 py-2 bg-blue-500/20 hover:bg-blue-500/30 text-blue-400 border border-blue-500/30 rounded text-sm"
|
|||
|
|
}, 'Копировать')
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'answer-data',
|
|||
|
|
className: "space-y-3 mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('textarea', {
|
|||
|
|
key: 'textarea',
|
|||
|
|
value: answerData,
|
|||
|
|
readOnly: true,
|
|||
|
|
rows: 6,
|
|||
|
|
className: "w-full p-3 bg-custom-bg border border-green-500/20 rounded-lg font-mono text-xs text-secondary resize-none custom-scrollbar"
|
|||
|
|
}),
|
|||
|
|
React.createElement(EnhancedCopyButton, {
|
|||
|
|
key: 'copy',
|
|||
|
|
text: answerData,
|
|||
|
|
className: "w-full px-3 py-2 bg-green-500/10 hover:bg-green-500/20 text-green-400 border border-green-500/20 rounded text-sm font-medium"
|
|||
|
|
}, 'Копировать зашифрованный ответ')
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'info',
|
|||
|
|
className: "p-3 bg-purple-500/10 border border-purple-500/20 rounded-lg"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('p', {
|
|||
|
|
className: "text-purple-400 text-sm flex items-center justify-center"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-shield-alt mr-2'
|
|||
|
|
}),
|
|||
|
|
'Соединение будет установлено с верификацией'
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Enhanced Chat Interface with better security indicators
|
|||
|
|
const EnhancedChatInterface = ({
|
|||
|
|
messages,
|
|||
|
|
messageInput,
|
|||
|
|
setMessageInput,
|
|||
|
|
onSendMessage,
|
|||
|
|
onDisconnect,
|
|||
|
|
keyFingerprint,
|
|||
|
|
isVerified,
|
|||
|
|
chatMessagesRef,
|
|||
|
|
scrollToBottom
|
|||
|
|
}) => {
|
|||
|
|
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
|||
|
|
|
|||
|
|
React.useEffect(() => {
|
|||
|
|
if (chatMessagesRef.current) {
|
|||
|
|
// Плавная прокрутка вниз
|
|||
|
|
chatMessagesRef.current.scrollTo({
|
|||
|
|
top: chatMessagesRef.current.scrollHeight,
|
|||
|
|
behavior: 'smooth'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}, [messages]);
|
|||
|
|
|
|||
|
|
// Обработчик прокрутки для показа кнопки
|
|||
|
|
const handleScroll = () => {
|
|||
|
|
if (chatMessagesRef.current) {
|
|||
|
|
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
|
|||
|
|
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
|
|||
|
|
setShowScrollButton(!isNearBottom);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Обертка для scrollToBottom с обновлением состояния кнопки
|
|||
|
|
const handleScrollToBottom = () => {
|
|||
|
|
scrollToBottom();
|
|||
|
|
setShowScrollButton(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleKeyPress = (e) => {
|
|||
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|||
|
|
e.preventDefault();
|
|||
|
|
onSendMessage();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
className: "chat-container"
|
|||
|
|
}, [
|
|||
|
|
// Chat Messages Area
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'chat-body',
|
|||
|
|
className: "chat-messages-area max-w-4xl mx-auto w-full p-4 relative"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'messages-container',
|
|||
|
|
ref: chatMessagesRef,
|
|||
|
|
onScroll: handleScroll,
|
|||
|
|
className: "h-full overflow-y-auto space-y-3 custom-scrollbar pr-2 scroll-smooth"
|
|||
|
|
},
|
|||
|
|
messages.length === 0 ?
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'empty-state',
|
|||
|
|
className: "flex items-center justify-center h-full"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
className: "text-center max-w-md"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'icon',
|
|||
|
|
className: "w-16 h-16 bg-green-500/10 border border-green-500/20 rounded-xl flex items-center justify-center mx-auto mb-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-comments text-2xl accent-green'
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
React.createElement('h3', {
|
|||
|
|
key: 'title',
|
|||
|
|
className: "text-lg font-medium text-primary mb-2"
|
|||
|
|
}, "Защищенный канал готов!"),
|
|||
|
|
React.createElement('p', {
|
|||
|
|
key: 'description',
|
|||
|
|
className: "text-secondary text-sm mb-4"
|
|||
|
|
}, "Все сообщения защищены современными криптографическими алгоритмами"),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'features',
|
|||
|
|
className: "text-left space-y-2"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f1',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-check accent-green mr-3'
|
|||
|
|
}),
|
|||
|
|
'End-to-end шифрование'
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f2',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-check accent-green mr-3'
|
|||
|
|
}),
|
|||
|
|
'Защита от replay атак'
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f3',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-check accent-green mr-3'
|
|||
|
|
}),
|
|||
|
|
'Верификация целостности'
|
|||
|
|
]),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'f4',
|
|||
|
|
className: "flex items-center text-sm text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-sync-alt accent-purple mr-3'
|
|||
|
|
}),
|
|||
|
|
'Perfect Forward Secrecy'
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]) :
|
|||
|
|
messages.map((msg) =>
|
|||
|
|
React.createElement(EnhancedChatMessage, {
|
|||
|
|
key: msg.id,
|
|||
|
|
message: msg.message,
|
|||
|
|
type: msg.type,
|
|||
|
|
timestamp: msg.timestamp
|
|||
|
|
})
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Кнопка прокрутки вниз
|
|||
|
|
showScrollButton && React.createElement('button', {
|
|||
|
|
key: 'scroll-down-btn',
|
|||
|
|
onClick: handleScrollToBottom,
|
|||
|
|
className: "absolute bottom-4 right-4 w-10 h-10 bg-gray-600/80 hover:bg-gray-500/80 text-white rounded-full flex items-center justify-center transition-all duration-200 shadow-lg",
|
|||
|
|
style: { zIndex: 10 }
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-chevron-down'
|
|||
|
|
})
|
|||
|
|
]),
|
|||
|
|
|
|||
|
|
// Enhanced Chat Input Area
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'chat-input',
|
|||
|
|
className: "chat-input-area border-t border-gray-500/10"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'input-container',
|
|||
|
|
className: "max-w-4xl mx-auto p-4"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'input-wrapper',
|
|||
|
|
className: "flex items-end space-x-3"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'text-area-wrapper',
|
|||
|
|
className: "flex-1 relative"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('textarea', {
|
|||
|
|
key: 'input',
|
|||
|
|
value: messageInput,
|
|||
|
|
onChange: (e) => setMessageInput(e.target.value),
|
|||
|
|
onKeyDown: handleKeyPress,
|
|||
|
|
placeholder: "Введите сообщение для шифрования...",
|
|||
|
|
rows: 2,
|
|||
|
|
maxLength: 2000,
|
|||
|
|
className: "w-full p-3 bg-gray-900/30 border border-gray-500/20 rounded-lg resize-none text-primary placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm"
|
|||
|
|
}),
|
|||
|
|
React.createElement('div', {
|
|||
|
|
key: 'input-info',
|
|||
|
|
className: "absolute bottom-2 right-3 flex items-center space-x-2 text-xs text-muted"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'counter'
|
|||
|
|
}, `${messageInput.length}/2000`),
|
|||
|
|
React.createElement('span', {
|
|||
|
|
key: 'hint'
|
|||
|
|
}, "• Enter для отправки")
|
|||
|
|
])
|
|||
|
|
]),
|
|||
|
|
React.createElement('button', {
|
|||
|
|
key: 'send',
|
|||
|
|
onClick: onSendMessage,
|
|||
|
|
disabled: !messageInput.trim(),
|
|||
|
|
className: "security-shield text-white p-3 rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
|||
|
|
}, [
|
|||
|
|
React.createElement('i', {
|
|||
|
|
className: 'fas fa-paper-plane'
|
|||
|
|
})
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
])
|
|||
|
|
]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Main Enhanced Application Component
|
|||
|
|
const EnhancedSecureP2PChat = () => {
|
|||
|
|
const [messages, setMessages] = React.useState([]);
|
|||
|
|
const [connectionStatus, setConnectionStatus] = React.useState('disconnected');
|
|||
|
|
const [messageInput, setMessageInput] = React.useState('');
|
|||
|
|
const [offerData, setOfferData] = React.useState('');
|
|||
|
|
const [answerData, setAnswerData] = React.useState('');
|
|||
|
|
const [offerInput, setOfferInput] = React.useState('');
|
|||
|
|
const [answerInput, setAnswerInput] = React.useState('');
|
|||
|
|
const [keyFingerprint, setKeyFingerprint] = React.useState('');
|
|||
|
|
const [verificationCode, setVerificationCode] = React.useState('');
|
|||
|
|
const [showOfferStep, setShowOfferStep] = React.useState(false);
|
|||
|
|
const [showAnswerStep, setShowAnswerStep] = React.useState(false);
|
|||
|
|
const [showVerification, setShowVerification] = React.useState(false);
|
|||
|
|
const [isVerified, setIsVerified] = React.useState(false);
|
|||
|
|
const [securityLevel, setSecurityLevel] = React.useState(null);
|
|||
|
|
|
|||
|
|
// Password modal state
|
|||
|
|
const [showPasswordModal, setShowPasswordModal] = React.useState(false);
|
|||
|
|
const [passwordInput, setPasswordInput] = React.useState('');
|
|||
|
|
const [passwordAction, setPasswordAction] = React.useState(null); // 'offer' or 'answer'
|
|||
|
|
const [passwordCallback, setPasswordCallback] = React.useState(null);
|
|||
|
|
|
|||
|
|
// Store generated passwords
|
|||
|
|
const [offerPassword, setOfferPassword] = React.useState('');
|
|||
|
|
const [answerPassword, setAnswerPassword] = React.useState('');
|
|||
|
|
|
|||
|
|
// Pay-per-session state
|
|||
|
|
const [sessionManager] = React.useState(() => new PayPerSessionManager());
|
|||
|
|
const [showPaymentModal, setShowPaymentModal] = React.useState(false);
|
|||
|
|
const [sessionTimeLeft, setSessionTimeLeft] = React.useState(0);
|
|||
|
|
const [pendingSession, setPendingSession] = React.useState(null); // { type, preimage }
|
|||
|
|
|
|||
|
|
const webrtcManagerRef = React.useRef(null);
|
|||
|
|
|
|||
|
|
// Update security level based on real verification
|
|||
|
|
const updateSecurityLevel = React.useCallback(async () => {
|
|||
|
|
if (webrtcManagerRef.current) {
|
|||
|
|
try {
|
|||
|
|
const level = await webrtcManagerRef.current.calculateSecurityLevel();
|
|||
|
|
setSecurityLevel(level);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Failed to update security level:', error);
|
|||
|
|
setSecurityLevel({
|
|||
|
|
level: 'ERROR',
|
|||
|
|
score: 0,
|
|||
|
|
color: 'red',
|
|||
|
|
details: 'Verification failed'
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// Session time ticker
|
|||
|
|
React.useEffect(() => {
|
|||
|
|
const timer = setInterval(() => {
|
|||
|
|
if (sessionManager.hasActiveSession()) {
|
|||
|
|
setSessionTimeLeft(sessionManager.getTimeLeft());
|
|||
|
|
} else {
|
|||
|
|
setSessionTimeLeft(0);
|
|||
|
|
}
|
|||
|
|
}, 1000);
|
|||
|
|
return () => clearInterval(timer);
|
|||
|
|
}, [sessionManager]);
|
|||
|
|
|
|||
|
|
// Session expiration handler
|
|||
|
|
React.useEffect(() => {
|
|||
|
|
sessionManager.onSessionExpired = () => {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '⏰ Время сессии истекло. Соединение будет разорвано.',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
setTimeout(() => handleDisconnect(), 3000);
|
|||
|
|
};
|
|||
|
|
}, [sessionManager]);
|
|||
|
|
const chatMessagesRef = React.useRef(null);
|
|||
|
|
|
|||
|
|
// Функция прокрутки вниз
|
|||
|
|
const scrollToBottom = () => {
|
|||
|
|
if (chatMessagesRef.current) {
|
|||
|
|
setTimeout(() => {
|
|||
|
|
chatMessagesRef.current.scrollTo({
|
|||
|
|
top: chatMessagesRef.current.scrollHeight,
|
|||
|
|
behavior: 'smooth'
|
|||
|
|
});
|
|||
|
|
}, 100);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Password modal functions
|
|||
|
|
const showPasswordPrompt = (action, callback) => {
|
|||
|
|
setPasswordAction(action);
|
|||
|
|
setPasswordCallback(() => callback);
|
|||
|
|
setShowPasswordModal(true);
|
|||
|
|
setPasswordInput('');
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handlePasswordSubmit = (password) => {
|
|||
|
|
setShowPasswordModal(false);
|
|||
|
|
if (passwordCallback) {
|
|||
|
|
passwordCallback(password);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handlePasswordCancel = () => {
|
|||
|
|
setShowPasswordModal(false);
|
|||
|
|
setPasswordInput('');
|
|||
|
|
setPasswordCallback(null);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
React.useEffect(() => {
|
|||
|
|
const handleMessage = (message, type) => {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message,
|
|||
|
|
type,
|
|||
|
|
id: Date.now() + Math.random(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
// Прокрутка при получении нового сообщения
|
|||
|
|
setTimeout(scrollToBottom, 100);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleStatusChange = (status) => {
|
|||
|
|
setConnectionStatus(status);
|
|||
|
|
|
|||
|
|
if (status === 'connected') {
|
|||
|
|
setIsVerified(true);
|
|||
|
|
setShowVerification(false);
|
|||
|
|
updateSecurityLevel().catch(console.error);
|
|||
|
|
} else if (status === 'verifying') {
|
|||
|
|
setShowVerification(true);
|
|||
|
|
updateSecurityLevel().catch(console.error);
|
|||
|
|
} else if (status === 'connecting') {
|
|||
|
|
updateSecurityLevel().catch(console.error);
|
|||
|
|
} else if (status === 'disconnected') {
|
|||
|
|
// Полная очистка UI при отключении
|
|||
|
|
setKeyFingerprint('');
|
|||
|
|
setVerificationCode('');
|
|||
|
|
setSecurityLevel(null);
|
|||
|
|
setIsVerified(false);
|
|||
|
|
setShowVerification(false);
|
|||
|
|
} else if (status === 'peer_disconnected') {
|
|||
|
|
// Небольшая задержка перед очисткой для показа статуса
|
|||
|
|
setTimeout(() => {
|
|||
|
|
setKeyFingerprint('');
|
|||
|
|
setVerificationCode('');
|
|||
|
|
setSecurityLevel(null);
|
|||
|
|
setIsVerified(false);
|
|||
|
|
setShowVerification(false);
|
|||
|
|
}, 2000);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
const handleKeyExchange = (fingerprint) => {
|
|||
|
|
if (fingerprint === '') {
|
|||
|
|
setKeyFingerprint('');
|
|||
|
|
} else {
|
|||
|
|
setKeyFingerprint(fingerprint);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleVerificationRequired = (code) => {
|
|||
|
|
if (code === '') {
|
|||
|
|
setVerificationCode('');
|
|||
|
|
} else {
|
|||
|
|
setVerificationCode(code);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Callback для обработки ошибок ответа
|
|||
|
|
const handleAnswerError = (errorType, errorMessage) => {
|
|||
|
|
console.log('Answer error callback:', errorType, errorMessage);
|
|||
|
|
|
|||
|
|
if (errorType === 'replay_attack') {
|
|||
|
|
// Сбрасываем сессию при replay attack
|
|||
|
|
if (sessionManager.hasActiveSession()) {
|
|||
|
|
sessionManager.resetSession();
|
|||
|
|
setSessionTimeLeft(0);
|
|||
|
|
}
|
|||
|
|
setPendingSession(null);
|
|||
|
|
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '💡 Данные устарели. Создайте новое приглашение или используйте актуальный код ответа',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
} else if (errorType === 'security_violation') {
|
|||
|
|
// Сбрасываем сессию при нарушении безопасности
|
|||
|
|
if (sessionManager.hasActiveSession()) {
|
|||
|
|
sessionManager.resetSession();
|
|||
|
|
setSessionTimeLeft(0);
|
|||
|
|
}
|
|||
|
|
setPendingSession(null);
|
|||
|
|
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `🔒 Нарушение безопасности: ${errorMessage}`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
webrtcManagerRef.current = new EnhancedSecureWebRTCManager(
|
|||
|
|
handleMessage,
|
|||
|
|
handleStatusChange,
|
|||
|
|
handleKeyExchange,
|
|||
|
|
handleVerificationRequired,
|
|||
|
|
handleAnswerError
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
handleMessage('🚀 LockBit.chat Enhanced Edition инициализирован. Готов к установке защищенного соединения с ECDH, шифрованным обменом и верификацией.', 'system');
|
|||
|
|
|
|||
|
|
// Cleanup on page unload
|
|||
|
|
const handleBeforeUnload = () => {
|
|||
|
|
if (webrtcManagerRef.current) {
|
|||
|
|
webrtcManagerRef.current.disconnect();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
window.addEventListener('beforeunload', handleBeforeUnload);
|
|||
|
|
|
|||
|
|
return () => {
|
|||
|
|
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|||
|
|
if (webrtcManagerRef.current) {
|
|||
|
|
webrtcManagerRef.current.disconnect();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
const ensureActiveSessionOrPurchase = async () => {
|
|||
|
|
if (sessionManager.hasActiveSession()) return true;
|
|||
|
|
if (pendingSession) return true; // уже куплено, активируем на коннекте
|
|||
|
|
setShowPaymentModal(true);
|
|||
|
|
return false;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleCreateOffer = async () => {
|
|||
|
|
try {
|
|||
|
|
const ok = await ensureActiveSessionOrPurchase();
|
|||
|
|
if (!ok) return;
|
|||
|
|
|
|||
|
|
setOfferData('');
|
|||
|
|
setShowOfferStep(false);
|
|||
|
|
|
|||
|
|
const offer = await webrtcManagerRef.current.createSecureOffer();
|
|||
|
|
|
|||
|
|
// Generate secure password for encryption
|
|||
|
|
const password = EnhancedSecureCryptoUtils.generateSecurePassword();
|
|||
|
|
|
|||
|
|
// Encrypt the offer data
|
|||
|
|
const encryptedOffer = await EnhancedSecureCryptoUtils.encryptData(offer, password);
|
|||
|
|
|
|||
|
|
setOfferData(encryptedOffer);
|
|||
|
|
setOfferPassword(password);
|
|||
|
|
setShowOfferStep(true);
|
|||
|
|
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '🔐 Защищенное приглашение создано и зашифровано!',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '📤 Отправьте зашифрованный код и пароль собеседнику по защищенному каналу (голос, SMS, etc).',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
updateSecurityLevel().catch(console.error);
|
|||
|
|
} catch (error) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `❌ Ошибка создания приглашения: ${error.message}`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleCreateAnswer = async () => {
|
|||
|
|
try {
|
|||
|
|
if (!offerInput.trim()) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '⚠️ Необходимо вставить зашифрованный код приглашения от собеседника',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Show password modal for offer decryption
|
|||
|
|
showPasswordPrompt('offer', async (password) => {
|
|||
|
|
if (!password) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '❌ Пароль не введен',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '🔄 Расшифровываю и обрабатываю защищенное приглашение...',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
setAnswerData('');
|
|||
|
|
setShowAnswerStep(false);
|
|||
|
|
|
|||
|
|
let offer;
|
|||
|
|
try {
|
|||
|
|
// Decrypt the offer data
|
|||
|
|
offer = await EnhancedSecureCryptoUtils.decryptData(offerInput.trim(), password);
|
|||
|
|
} catch (decryptError) {
|
|||
|
|
throw new Error(`Ошибка расшифровки: ${decryptError.message}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!offer || typeof offer !== 'object') {
|
|||
|
|
throw new Error('Приглашение должно быть объектом');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!offer.type || offer.type !== 'enhanced_secure_offer') {
|
|||
|
|
throw new Error('Неверный тип приглашения. Ожидается enhanced_secure_offer');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('Creating secure answer for offer:', offer);
|
|||
|
|
const answer = await webrtcManagerRef.current.createSecureAnswer(offer);
|
|||
|
|
console.log('Secure answer created:', answer);
|
|||
|
|
|
|||
|
|
// Generate new password for answer encryption
|
|||
|
|
const answerPassword = EnhancedSecureCryptoUtils.generateSecurePassword();
|
|||
|
|
|
|||
|
|
// Encrypt the answer data
|
|||
|
|
const encryptedAnswer = await EnhancedSecureCryptoUtils.encryptData(answer, answerPassword);
|
|||
|
|
|
|||
|
|
setAnswerData(encryptedAnswer);
|
|||
|
|
setAnswerPassword(answerPassword); // Store the password
|
|||
|
|
setShowAnswerStep(true);
|
|||
|
|
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '✅ Защищенный ответ создан и зашифрован!',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '📤 Отправьте зашифрованный код ответа и пароль инициатору по защищенному каналу.',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
// Update security level after creating answer
|
|||
|
|
updateSecurityLevel().catch(console.error);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error in handleCreateAnswer:', error);
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `❌ Ошибка обработки приглашения: ${error.message}`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return; // Exit early, callback will handle the rest
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error in handleCreateAnswer:', error);
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `❌ Ошибка обработки приглашения: ${error.message}`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleConnect = async () => {
|
|||
|
|
try {
|
|||
|
|
if (!answerInput.trim()) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '⚠️ Необходимо вставить зашифрованный код ответа от собеседника',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Show password modal for answer decryption
|
|||
|
|
showPasswordPrompt('answer', async (password) => {
|
|||
|
|
if (!password) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '❌ Пароль не введен',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '🔄 Расшифровываю и обрабатываю защищенный ответ...',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
let answer;
|
|||
|
|
try {
|
|||
|
|
// Decrypt the answer data
|
|||
|
|
answer = await EnhancedSecureCryptoUtils.decryptData(answerInput.trim(), password);
|
|||
|
|
} catch (decryptError) {
|
|||
|
|
throw new Error(`Ошибка расшифровки: ${decryptError.message}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!answer || typeof answer !== 'object') {
|
|||
|
|
throw new Error('Ответ должен быть объектом');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!answer.type || answer.type !== 'enhanced_secure_answer') {
|
|||
|
|
throw new Error('Неверный тип ответа. Ожидается enhanced_secure_answer');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await webrtcManagerRef.current.handleSecureAnswer(answer);
|
|||
|
|
|
|||
|
|
// Активируем сессию ТОЛЬКО после успешной обработки ответа
|
|||
|
|
if (sessionManager.canActivateSession() && pendingSession) {
|
|||
|
|
const result = sessionManager.safeActivateSession(pendingSession.type, pendingSession.preimage, pendingSession.paymentHash);
|
|||
|
|
if (result.success) {
|
|||
|
|
setPendingSession(null);
|
|||
|
|
setSessionTimeLeft(sessionManager.getTimeLeft()); // Обновляем таймер
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `💰 Сессия активирована на ${sessionManager.sessionPrices[pendingSession.type].hours}ч (${result.method})`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
} else {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `❌ Ошибка активации сессии: ${result.reason}`,
|
|||
|
|
type: 'error',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '🔄 Финализация защищенного соединения...',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
// Update security level after handling answer
|
|||
|
|
updateSecurityLevel().catch(console.error);
|
|||
|
|
} catch (error) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `❌ Ошибка установки соединения: ${error.message}`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
// Обработка ошибок теперь происходит в callback handleAnswerError
|
|||
|
|
// Дополнительная обработка только для общих ошибок
|
|||
|
|
if (!error.message.includes('слишком старые') && !error.message.includes('too old')) {
|
|||
|
|
setPendingSession(null);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return; // Exit early, callback will handle the rest
|
|||
|
|
} catch (error) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `❌ Ошибка установки соединения: ${error.message}`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
// Обработка ошибок теперь происходит в callback handleAnswerError
|
|||
|
|
// Дополнительная обработка только для общих ошибок
|
|||
|
|
if (!error.message.includes('слишком старые') && !error.message.includes('too old')) {
|
|||
|
|
setPendingSession(null);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleVerifyConnection = (isValid) => {
|
|||
|
|
if (isValid) {
|
|||
|
|
webrtcManagerRef.current.confirmVerification();
|
|||
|
|
} else {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '❌ Верификация отклонена. Соединение небезопасно! Сессия сброшена.',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
// Сбрасываем сессию при неудачной верификации
|
|||
|
|
sessionManager.resetSession();
|
|||
|
|
setSessionTimeLeft(0);
|
|||
|
|
setPendingSession(null);
|
|||
|
|
|
|||
|
|
handleDisconnect();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSendMessage = async () => {
|
|||
|
|
if (!messageInput.trim() || !webrtcManagerRef.current.isConnected()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await webrtcManagerRef.current.sendSecureMessage(messageInput);
|
|||
|
|
setMessageInput('');
|
|||
|
|
// Прокрутка после отправки сообщения
|
|||
|
|
setTimeout(scrollToBottom, 50);
|
|||
|
|
} catch (error) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `❌ Ошибка отправки: ${error.message}`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
// Прокрутка после ошибки
|
|||
|
|
setTimeout(scrollToBottom, 50);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleClearData = () => {
|
|||
|
|
setOfferData('');
|
|||
|
|
setAnswerData('');
|
|||
|
|
setOfferInput('');
|
|||
|
|
setAnswerInput('');
|
|||
|
|
setShowOfferStep(false);
|
|||
|
|
setShowAnswerStep(false);
|
|||
|
|
setShowVerification(false);
|
|||
|
|
setVerificationCode('');
|
|||
|
|
setIsVerified(false);
|
|||
|
|
setKeyFingerprint('');
|
|||
|
|
setSecurityLevel(null);
|
|||
|
|
setConnectionStatus('disconnected');
|
|||
|
|
setMessages([]);
|
|||
|
|
setMessageInput('');
|
|||
|
|
setOfferPassword(''); // Clear offer password
|
|||
|
|
setAnswerPassword(''); // Clear answer password
|
|||
|
|
|
|||
|
|
if (typeof console.clear === 'function') {
|
|||
|
|
console.clear();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Cleanup pay-per-session state
|
|||
|
|
sessionManager.cleanup();
|
|||
|
|
setSessionTimeLeft(0);
|
|||
|
|
setShowPaymentModal(false);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDisconnect = () => {
|
|||
|
|
// Cleanup pay-per-session state
|
|||
|
|
sessionManager.cleanup();
|
|||
|
|
setSessionTimeLeft(0);
|
|||
|
|
setShowPaymentModal(false);
|
|||
|
|
|
|||
|
|
webrtcManagerRef.current.disconnect();
|
|||
|
|
|
|||
|
|
// Полная очистка UI состояния
|
|||
|
|
setKeyFingerprint('');
|
|||
|
|
setVerificationCode('');
|
|||
|
|
setSecurityLevel(null);
|
|||
|
|
setIsVerified(false);
|
|||
|
|
setShowVerification(false);
|
|||
|
|
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '🔌 Защищенное соединение разорвано',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
|
|||
|
|
handleClearData();
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleSessionActivated = (session) => {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `💰 Сессия активирована на ${sessionManager.sessionPrices[session.type].hours}ч. Можете создавать приглашения!`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
React.useEffect(() => {
|
|||
|
|
if (connectionStatus === 'connected' && isVerified) {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: '🎉 Защищенное соединение успешно установлено и верифицировано! Теперь вы можете безопасно общаться с полной защитой от MITM атак и Perfect Forward Secrecy.',
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
// Прокрутка при подключении
|
|||
|
|
setTimeout(scrollToBottom, 200);
|
|||
|
|
}
|
|||
|
|
}, [connectionStatus, isVerified]);
|
|||
|
|
|
|||
|
|
const isConnectedAndVerified = connectionStatus === 'connected' && isVerified;
|
|||
|
|
|
|||
|
|
// Активировать сессию при коннекте инициатора (когда мы создаем offer и получаем connected)
|
|||
|
|
React.useEffect(() => {
|
|||
|
|
if (isConnectedAndVerified && sessionManager.canActivateSession() && pendingSession && connectionStatus !== 'failed') {
|
|||
|
|
const result = sessionManager.safeActivateSession(pendingSession.type, pendingSession.preimage, pendingSession.paymentHash);
|
|||
|
|
if (result.success) {
|
|||
|
|
setPendingSession(null);
|
|||
|
|
setSessionTimeLeft(sessionManager.getTimeLeft()); // Обновляем таймер
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `💰 Сессия активирована на ${sessionManager.sessionPrices[pendingSession.type].hours}ч (${result.method})`,
|
|||
|
|
type: 'system',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
} else {
|
|||
|
|
setMessages(prev => [...prev, {
|
|||
|
|
message: `❌ Ошибка активации сессии: ${result.reason}`,
|
|||
|
|
type: 'error',
|
|||
|
|
id: Date.now(),
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
}]);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, [isConnectedAndVerified, sessionManager, pendingSession, connectionStatus]);
|
|||
|
|
|
|||
|
|
return React.createElement('div', {
|
|||
|
|
className: "minimal-bg min-h-screen"
|
|||
|
|
}, [
|
|||
|
|
React.createElement(EnhancedMinimalHeader, {
|
|||
|
|
key: 'header',
|
|||
|
|
status: connectionStatus,
|
|||
|
|
fingerprint: keyFingerprint,
|
|||
|
|
verificationCode: verificationCode,
|
|||
|
|
onDisconnect: handleDisconnect,
|
|||
|
|
isConnected: isConnectedAndVerified,
|
|||
|
|
securityLevel: securityLevel,
|
|||
|
|
sessionManager: sessionManager,
|
|||
|
|
sessionTimeLeft: sessionTimeLeft
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
React.createElement('main', {
|
|||
|
|
key: 'main'
|
|||
|
|
},
|
|||
|
|
isConnectedAndVerified
|
|||
|
|
? React.createElement(EnhancedChatInterface, {
|
|||
|
|
messages: messages,
|
|||
|
|
messageInput: messageInput,
|
|||
|
|
setMessageInput: setMessageInput,
|
|||
|
|
onSendMessage: handleSendMessage,
|
|||
|
|
onDisconnect: handleDisconnect,
|
|||
|
|
keyFingerprint: keyFingerprint,
|
|||
|
|
isVerified: isVerified,
|
|||
|
|
chatMessagesRef: chatMessagesRef,
|
|||
|
|
scrollToBottom: scrollToBottom
|
|||
|
|
})
|
|||
|
|
: React.createElement(EnhancedConnectionSetup, {
|
|||
|
|
onCreateOffer: handleCreateOffer,
|
|||
|
|
onCreateAnswer: handleCreateAnswer,
|
|||
|
|
onConnect: handleConnect,
|
|||
|
|
onClearData: handleClearData,
|
|||
|
|
onVerifyConnection: handleVerifyConnection,
|
|||
|
|
connectionStatus: connectionStatus,
|
|||
|
|
offerData: offerData,
|
|||
|
|
answerData: answerData,
|
|||
|
|
offerInput: offerInput,
|
|||
|
|
setOfferInput: setOfferInput,
|
|||
|
|
answerInput: answerInput,
|
|||
|
|
setAnswerInput: setAnswerInput,
|
|||
|
|
showOfferStep: showOfferStep,
|
|||
|
|
showAnswerStep: showAnswerStep,
|
|||
|
|
verificationCode: verificationCode,
|
|||
|
|
showVerification: showVerification,
|
|||
|
|
messages: messages,
|
|||
|
|
offerPassword: offerPassword,
|
|||
|
|
answerPassword: answerPassword
|
|||
|
|
})
|
|||
|
|
),
|
|||
|
|
|
|||
|
|
// Password Modal
|
|||
|
|
React.createElement(PasswordModal, {
|
|||
|
|
key: 'password-modal',
|
|||
|
|
isOpen: showPasswordModal,
|
|||
|
|
onClose: handlePasswordCancel,
|
|||
|
|
onSubmit: handlePasswordSubmit,
|
|||
|
|
action: passwordAction,
|
|||
|
|
password: passwordInput,
|
|||
|
|
setPassword: setPasswordInput
|
|||
|
|
}),
|
|||
|
|
|
|||
|
|
// Payment Modal
|
|||
|
|
React.createElement(PaymentModal, {
|
|||
|
|
key: 'payment-modal',
|
|||
|
|
isOpen: showPaymentModal,
|
|||
|
|
onClose: () => setShowPaymentModal(false),
|
|||
|
|
sessionManager: sessionManager,
|
|||
|
|
onSessionPurchased: (session) => { setPendingSession(session); handleSessionActivated({ type: session.type }); }
|
|||
|
|
})
|
|||
|
|
]);
|
|||
|
|
};
|
|||
|
|
function initializeApp() {
|
|||
|
|
// Проверяем что оба модуля загружены
|
|||
|
|
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
|
|||
|
|
console.log('🚀 Инициализируем React приложение...');
|
|||
|
|
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
|||
|
|
} else {
|
|||
|
|
console.error('❌ Модули не загружены:', {
|
|||
|
|
hasCrypto: !!window.EnhancedSecureCryptoUtils,
|
|||
|
|
hasWebRTC: !!window.EnhancedSecureWebRTCManager
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Render Enhanced Application
|
|||
|
|
// ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<script type="module">
|
|||
|
|
try {
|
|||
|
|
const timestamp = Date.now();
|
|||
|
|
|
|||
|
|
const [cryptoModule, webrtcModule, paymentModule] = await Promise.all([
|
|||
|
|
import(`./src/crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
|
|||
|
|
import(`./src/network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
|
|||
|
|
import(`./src/session/PayPerSessionManager.js?v=${timestamp}`)
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
// Добавьте эти строки:
|
|||
|
|
const { EnhancedSecureCryptoUtils } = cryptoModule;
|
|||
|
|
window.EnhancedSecureCryptoUtils = EnhancedSecureCryptoUtils;
|
|||
|
|
|
|||
|
|
const { EnhancedSecureWebRTCManager } = webrtcModule;
|
|||
|
|
window.EnhancedSecureWebRTCManager = EnhancedSecureWebRTCManager;
|
|||
|
|
|
|||
|
|
const { PayPerSessionManager } = paymentModule;
|
|||
|
|
window.PayPerSessionManager = PayPerSessionManager;
|
|||
|
|
|
|||
|
|
// Функция загрузки компонентов React через fetch
|
|||
|
|
async function loadReactComponent(path, componentName) {
|
|||
|
|
try {
|
|||
|
|
console.log(`🔄 Загружаем компонент ${componentName} из ${path}`);
|
|||
|
|
|
|||
|
|
const response = await fetch(`${path}?v=${timestamp}`);
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const componentCode = await response.text();
|
|||
|
|
|
|||
|
|
// Выполняем код компонента в глобальном контексте
|
|||
|
|
eval(componentCode);
|
|||
|
|
|
|||
|
|
// Проверяем, что компонент загружен
|
|||
|
|
if (window[componentName]) {
|
|||
|
|
console.log(`✅ Компонент ${componentName} успешно загружен`);
|
|||
|
|
} else {
|
|||
|
|
throw new Error(`Компонент ${componentName} не найден в window после загрузки`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error(`❌ Ошибка загрузки компонента ${componentName}:`, error);
|
|||
|
|
throw error;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Загружаем UI компоненты поэтапно
|
|||
|
|
console.log('🚀 Начинаем загрузку UI компонентов...');
|
|||
|
|
|
|||
|
|
await Promise.all([
|
|||
|
|
loadReactComponent('./src/components/ui/SessionTimer.jsx', 'SessionTimer'),
|
|||
|
|
loadReactComponent('./src/components/ui/Header.jsx', 'EnhancedMinimalHeader'),
|
|||
|
|
loadReactComponent('./src/components/ui/PasswordModal.jsx', 'PasswordModal'),
|
|||
|
|
loadReactComponent('./src/components/ui/SessionTypeSelector.jsx', 'SessionTypeSelector'),
|
|||
|
|
loadReactComponent('./src/components/ui/LightningPayment.jsx', 'LightningPayment'),
|
|||
|
|
loadReactComponent('./src/components/ui/PaymentModal.jsx', 'PaymentModal')
|
|||
|
|
]);
|
|||
|
|
console.log('🎉 UI компоненты успешно загружены!');
|
|||
|
|
console.log('📋 Доступные компоненты:', {
|
|||
|
|
EnhancedMinimalHeader: !!window.EnhancedMinimalHeader,
|
|||
|
|
SessionTimer: !!window.SessionTimer
|
|||
|
|
});
|
|||
|
|
if (typeof initializeApp === 'function') {
|
|||
|
|
initializeApp();
|
|||
|
|
} else {
|
|||
|
|
console.error('❌ Функция initializeApp не найдена');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('❌ Ошибка загрузки модуля:', error);
|
|||
|
|
console.error('📍 Стек:', error.stack);
|
|||
|
|
|
|||
|
|
// Показываем подробную ошибку на странице
|
|||
|
|
document.getElementById('root').innerHTML = `
|
|||
|
|
<div style="padding: 20px; color: #ef4444; font-family: monospace; background: #1a1a1a; min-height: 100vh;">
|
|||
|
|
<h2>❌ Ошибка загрузки модуля</h2>
|
|||
|
|
<p><strong>Ошибка:</strong> ${error.message}</p>
|
|||
|
|
|
|||
|
|
<div style="margin-top: 20px; padding: 15px; background: rgba(245, 158, 11, 0.1); border-radius: 8px;">
|
|||
|
|
<h3 style="color: #f59e0b;">🔧 Возможные решения:</h3>
|
|||
|
|
<ol style="color: #f59e0b;">
|
|||
|
|
<li>Убедитесь, что файлы находятся в src/components/ui/</li>
|
|||
|
|
<li>Проверьте SessionTimer.jsx и Header.jsx</li>
|
|||
|
|
<li>Убедитесь, что файлы используют window.React</li>
|
|||
|
|
<li>Перезагрузите страницу (Ctrl+F5)</li>
|
|||
|
|
<li>Откройте Developer Tools и проверьте Network tab</li>
|
|||
|
|
</ol>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<pre style="background: rgba(0,0,0,0.3); padding: 10px; border-radius: 5px; margin-top: 15px; overflow: auto;">${error.stack}</pre>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|