Browser extension for SecureBit Chat — a P2P messenger with military-grade cryptography.
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
// Simple QR Scanner Component using only Html5Qrcode
|
||||
const QRScanner = ({ onScan, onClose, isVisible, continuous = false }) => {
|
||||
const videoRef = React.useRef(null);
|
||||
const qrScannerRef = React.useRef(null);
|
||||
const [error, setError] = React.useState(null);
|
||||
const [isScanning, setIsScanning] = React.useState(false);
|
||||
const [progress, setProgress] = React.useState({ id: null, seq: 0, total: 0 });
|
||||
const [showFocusHint, setShowFocusHint] = React.useState(false);
|
||||
const [manualMode, setManualMode] = React.useState(false);
|
||||
const [scannedParts, setScannedParts] = React.useState(new Set());
|
||||
const [currentQRId, setCurrentQRId] = React.useState(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isVisible) {
|
||||
startScanner();
|
||||
} else {
|
||||
stopScanner();
|
||||
}
|
||||
|
||||
return () => {
|
||||
stopScanner();
|
||||
};
|
||||
}, [isVisible]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const onProgress = (e) => {
|
||||
const { id, seq, total } = e.detail || {};
|
||||
if (!id || !total) return;
|
||||
setProgress({ id, seq, total });
|
||||
|
||||
// Обновляем ID текущего QR кода
|
||||
if (id !== currentQRId) {
|
||||
setCurrentQRId(id);
|
||||
setScannedParts(new Set()); // Сбрасываем сканированные части для нового ID
|
||||
}
|
||||
|
||||
// Добавляем отсканированную часть
|
||||
setScannedParts(prev => new Set([...prev, seq]));
|
||||
};
|
||||
const onComplete = () => {
|
||||
// Close scanner once app signals completion
|
||||
if (!continuous) return;
|
||||
try { stopScanner(); } catch {}
|
||||
};
|
||||
document.addEventListener('qr-scan-progress', onProgress, { passive: true });
|
||||
document.addEventListener('qr-scan-complete', onComplete, { passive: true });
|
||||
return () => {
|
||||
document.removeEventListener('qr-scan-progress', onProgress, { passive: true });
|
||||
document.removeEventListener('qr-scan-complete', onComplete, { passive: true });
|
||||
};
|
||||
}, [currentQRId]);
|
||||
|
||||
// Функция для tap-to-focus
|
||||
const handleTapToFocus = (event, html5Qrcode) => {
|
||||
try {
|
||||
// Показываем подсказку о фокусировке
|
||||
setShowFocusHint(true);
|
||||
setTimeout(() => setShowFocusHint(false), 2000);
|
||||
|
||||
// Получаем координаты клика относительно видео элемента
|
||||
const rect = event.target.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
// Нормализуем координаты (0-1)
|
||||
const normalizedX = x / rect.width;
|
||||
const normalizedY = y / rect.height;
|
||||
|
||||
console.log('Tap to focus at:', { x, y, normalizedX, normalizedY });
|
||||
|
||||
// Попытка программной фокусировки (если поддерживается браузером)
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
// Это может не работать во всех браузерах, но попробуем
|
||||
console.log('Attempting programmatic focus...');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Tap to focus error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Функции для ручного управления
|
||||
const toggleManualMode = () => {
|
||||
setManualMode(!manualMode);
|
||||
if (!manualMode) {
|
||||
// При включении ручного режима останавливаем автопрокрутку
|
||||
console.log('Manual mode enabled - auto-scroll disabled');
|
||||
} else {
|
||||
// При выключении ручного режима возобновляем автопрокрутку
|
||||
console.log('Manual mode disabled - auto-scroll enabled');
|
||||
}
|
||||
};
|
||||
|
||||
const resetProgress = () => {
|
||||
setScannedParts(new Set());
|
||||
setCurrentQRId(null);
|
||||
setProgress({ id: null, seq: 0, total: 0 });
|
||||
};
|
||||
|
||||
const startScanner = async () => {
|
||||
try {
|
||||
console.log('Starting QR scanner...');
|
||||
setError(null);
|
||||
setIsScanning(true);
|
||||
|
||||
// Allow camera on HTTP as well; rely on browser permission prompts
|
||||
|
||||
// Check if Html5Qrcode is available
|
||||
if (!window.Html5Qrcode) {
|
||||
setError('QR scanner library not loaded');
|
||||
setIsScanning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get available cameras first
|
||||
console.log('Getting available cameras...');
|
||||
const cameras = await window.Html5Qrcode.getCameras();
|
||||
console.log('Available cameras:', cameras);
|
||||
|
||||
if (!cameras || cameras.length === 0) {
|
||||
setError('No cameras found on this device');
|
||||
setIsScanning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any existing scanner
|
||||
if (qrScannerRef.current) {
|
||||
try {
|
||||
qrScannerRef.current.stop();
|
||||
} catch (e) {
|
||||
console.log('Stopping previous scanner:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Create video element if it doesn't exist
|
||||
if (!videoRef.current) {
|
||||
console.log('Video element not found');
|
||||
setError('Video element not found');
|
||||
setIsScanning(false);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Video element found:', videoRef.current);
|
||||
console.log('Video element ID:', videoRef.current.id);
|
||||
|
||||
// Create Html5Qrcode instance
|
||||
console.log('Creating Html5Qrcode instance...');
|
||||
const html5Qrcode = new window.Html5Qrcode(videoRef.current.id || 'qr-reader');
|
||||
|
||||
// Find back camera (environment facing)
|
||||
let cameraId = cameras[0].id; // Default to first camera
|
||||
let selectedCamera = cameras[0];
|
||||
|
||||
// Look for back camera
|
||||
for (const camera of cameras) {
|
||||
if (camera.label.toLowerCase().includes('back') ||
|
||||
camera.label.toLowerCase().includes('rear') ||
|
||||
camera.label.toLowerCase().includes('environment')) {
|
||||
cameraId = camera.id;
|
||||
selectedCamera = camera;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Available cameras:');
|
||||
cameras.forEach((cam, index) => {
|
||||
console.log(`${index + 1}. ${cam.label} (${cam.id})`);
|
||||
});
|
||||
console.log('Selected camera:', selectedCamera.label, 'ID:', cameraId);
|
||||
|
||||
// Start camera
|
||||
console.log('Starting camera with Html5Qrcode...');
|
||||
const isDesktop = (typeof window !== 'undefined') && ((window.innerWidth || 0) >= 1024);
|
||||
const qrboxSize = isDesktop ? 560 : 360;
|
||||
await html5Qrcode.start(
|
||||
cameraId, // Use specific camera ID
|
||||
{
|
||||
fps: /iPhone|iPad|iPod/i.test(navigator.userAgent) ? 2 : 3,
|
||||
qrbox: { width: qrboxSize, height: qrboxSize },
|
||||
// Улучшенные настройки для мобильных устройств
|
||||
aspectRatio: 1.0,
|
||||
videoConstraints: {
|
||||
focusMode: "continuous", // Непрерывная автофокусировка
|
||||
exposureMode: "continuous", // Непрерывная экспозиция
|
||||
whiteBalanceMode: "continuous", // Непрерывный баланс белого
|
||||
torch: false, // Вспышка выключена по умолчанию
|
||||
facingMode: "environment" // Используем заднюю камеру
|
||||
}
|
||||
},
|
||||
(decodedText, decodedResult) => {
|
||||
console.log('QR Code detected:', decodedText);
|
||||
try {
|
||||
const res = onScan(decodedText);
|
||||
const handleResult = (val) => {
|
||||
const shouldClose = val === true || !continuous;
|
||||
if (shouldClose) {
|
||||
stopScanner();
|
||||
}
|
||||
};
|
||||
if (res && typeof res.then === 'function') {
|
||||
res.then(handleResult).catch((e) => {
|
||||
console.warn('onScan async handler error:', e);
|
||||
if (!continuous) stopScanner();
|
||||
});
|
||||
} else {
|
||||
handleResult(res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('onScan handler threw:', e);
|
||||
if (!continuous) {
|
||||
stopScanner();
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// Ignore decode errors, they're normal during scanning
|
||||
console.log('QR decode error:', error);
|
||||
}
|
||||
);
|
||||
|
||||
// Store scanner reference
|
||||
qrScannerRef.current = html5Qrcode;
|
||||
console.log('QR scanner started successfully');
|
||||
|
||||
// Добавляем обработчик tap-to-focus для мобильных устройств
|
||||
if (videoRef.current) {
|
||||
videoRef.current.addEventListener('click', (event) => {
|
||||
handleTapToFocus(event, html5Qrcode);
|
||||
});
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error starting QR scanner:', err);
|
||||
let errorMessage = 'Failed to start camera';
|
||||
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMessage = 'Camera access denied. Please allow camera access and try again.';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMessage = 'No camera found on this device.';
|
||||
} else if (err.name === 'NotSupportedError') {
|
||||
errorMessage = 'Camera not supported on this device.';
|
||||
} else if (err.name === 'NotReadableError') {
|
||||
errorMessage = 'Camera is already in use by another application.';
|
||||
} else if (err.message) {
|
||||
errorMessage = err.message;
|
||||
}
|
||||
|
||||
setError(errorMessage);
|
||||
setIsScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
const stopScanner = () => {
|
||||
if (qrScannerRef.current) {
|
||||
try {
|
||||
qrScannerRef.current.stop().then(() => {
|
||||
console.log('QR scanner stopped');
|
||||
}).catch((err) => {
|
||||
console.log('Error stopping scanner:', err);
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Error stopping scanner:', err);
|
||||
}
|
||||
qrScannerRef.current = null;
|
||||
}
|
||||
setIsScanning(false);
|
||||
try {
|
||||
// iOS Safari workaround: small delay before closing modal to release camera
|
||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
setTimeout(() => {
|
||||
// no-op; allow camera to settle
|
||||
}, 150);
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
stopScanner();
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return React.createElement('div', {
|
||||
className: "fixed inset-0 bg-black/80 flex items-center justify-center z-50"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'scanner-modal',
|
||||
className: "bg-gray-800 rounded-lg p-6 w-full mx-4 max-w-2xl"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'scanner-header',
|
||||
className: "flex items-center justify-between mb-4"
|
||||
}, [
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: "text-lg font-medium text-white"
|
||||
}, 'Scan QR Code'),
|
||||
React.createElement('button', {
|
||||
key: 'close-btn',
|
||||
onClick: handleClose,
|
||||
className: "text-gray-400 hover:text-white transition-colors"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times text-xl'
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Индикатор прогресса сканирования
|
||||
progress.total > 1 && React.createElement('div', {
|
||||
key: 'progress-indicator',
|
||||
className: "mb-4 p-3 bg-gray-800/50 border border-gray-600/30 rounded-lg"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-header',
|
||||
className: "flex items-center justify-between mb-2"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'progress-title',
|
||||
className: "text-sm text-gray-300"
|
||||
}, `QR ID: ${currentQRId ? currentQRId.substring(0, 8) + '...' : 'N/A'}`),
|
||||
React.createElement('span', {
|
||||
key: 'progress-count',
|
||||
className: "text-sm text-blue-400"
|
||||
}, `${scannedParts.size}/${progress.total} scanned`)
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'progress-numbers',
|
||||
className: "flex flex-wrap gap-1"
|
||||
}, Array.from({ length: progress.total }, (_, i) => {
|
||||
const partNumber = i + 1;
|
||||
const isScanned = scannedParts.has(partNumber);
|
||||
return React.createElement('div', {
|
||||
key: `part-${partNumber}`,
|
||||
className: `w-8 h-8 rounded-full flex items-center justify-center text-xs font-medium transition-colors ${
|
||||
isScanned
|
||||
? 'bg-green-500 text-white'
|
||||
: 'bg-gray-600 text-gray-300'
|
||||
}`
|
||||
}, partNumber);
|
||||
}))
|
||||
]),
|
||||
|
||||
// Панель управления
|
||||
progress.total > 1 && React.createElement('div', {
|
||||
key: 'control-panel',
|
||||
className: "mb-4 flex gap-2"
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'manual-toggle',
|
||||
onClick: toggleManualMode,
|
||||
className: `px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
manualMode
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-600 text-gray-300 hover:bg-gray-500'
|
||||
}`
|
||||
}, manualMode ? 'Manual Mode' : 'Auto Mode'),
|
||||
React.createElement('button', {
|
||||
key: 'reset-progress',
|
||||
onClick: resetProgress,
|
||||
className: "px-3 py-1 bg-red-500/20 text-red-400 border border-red-500/20 rounded text-xs font-medium hover:bg-red-500/30"
|
||||
}, 'Reset'),
|
||||
React.createElement('span', {
|
||||
key: 'mode-hint',
|
||||
className: "text-xs text-gray-400 self-center"
|
||||
}, manualMode ? 'Tap to focus, scan manually' : 'Auto-scrolling enabled')
|
||||
]),
|
||||
|
||||
React.createElement('div', {
|
||||
key: 'scanner-content',
|
||||
className: "relative"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'video-container',
|
||||
id: 'qr-reader',
|
||||
ref: videoRef,
|
||||
className: "w-full h-80 md:h-[32rem] bg-gray-700 rounded-lg"
|
||||
}),
|
||||
|
||||
error && React.createElement('div', {
|
||||
key: 'error',
|
||||
className: "absolute inset-0 flex items-center justify-center bg-red-900/50 rounded-lg"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'error-content',
|
||||
className: "text-center text-white p-4"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'error-icon',
|
||||
className: 'fas fa-exclamation-triangle text-2xl mb-2'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'error-text',
|
||||
className: "text-sm"
|
||||
}, error)
|
||||
])
|
||||
]),
|
||||
|
||||
!error && !isScanning && React.createElement('div', {
|
||||
key: 'loading',
|
||||
className: "absolute inset-0 flex items-center justify-center bg-gray-700/50 rounded-lg"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'loading-content',
|
||||
className: "text-center text-white"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'loading-icon',
|
||||
className: 'fas fa-spinner fa-spin text-2xl mb-2'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'loading-text',
|
||||
className: "text-sm"
|
||||
}, 'Starting camera...')
|
||||
])
|
||||
]),
|
||||
|
||||
!error && isScanning && React.createElement('div', {
|
||||
key: 'scanning-overlay',
|
||||
className: "absolute inset-0 flex items-center justify-center"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'scanning-content',
|
||||
className: "text-center text-white bg-black/50 rounded-lg px-4 py-2"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'scanning-icon',
|
||||
className: 'fas fa-qrcode text-xl mb-1'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'scanning-text',
|
||||
className: "text-xs"
|
||||
}, progress && progress.total > 1 ? `Frames: ${Math.min(progress.seq, progress.total)}/${progress.total}` : 'Point camera at QR code'),
|
||||
React.createElement('p', {
|
||||
key: 'tap-hint',
|
||||
className: "text-xs text-blue-300 mt-1"
|
||||
}, 'Tap screen to focus')
|
||||
])
|
||||
]),
|
||||
|
||||
// Подсказка о фокусировке
|
||||
showFocusHint && React.createElement('div', {
|
||||
key: 'focus-hint',
|
||||
className: "absolute top-4 left-1/2 transform -translate-x-1/2 bg-green-500/90 text-white px-3 py-1 rounded-full text-xs font-medium z-10"
|
||||
}, 'Focusing...'),
|
||||
// Bottom overlay kept simple on mobile
|
||||
]),
|
||||
|
||||
// Дополнительные подсказки для улучшения сканирования
|
||||
React.createElement('div', {
|
||||
key: 'scanning-tips',
|
||||
className: "mt-4 p-3 bg-blue-500/10 border border-blue-500/20 rounded-lg"
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'tips-title',
|
||||
className: "text-blue-400 text-sm font-medium mb-2 flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'tips-icon',
|
||||
className: 'fas fa-lightbulb mr-2'
|
||||
}),
|
||||
'Tips for better scanning:'
|
||||
]),
|
||||
React.createElement('ul', {
|
||||
key: 'tips-list',
|
||||
className: "text-xs text-blue-300 space-y-1"
|
||||
}, [
|
||||
React.createElement('li', {
|
||||
key: 'tip-1'
|
||||
}, '• Ensure good lighting'),
|
||||
React.createElement('li', {
|
||||
key: 'tip-2'
|
||||
}, '• Hold phone steady'),
|
||||
React.createElement('li', {
|
||||
key: 'tip-3'
|
||||
}, '• Tap screen to focus'),
|
||||
React.createElement('li', {
|
||||
key: 'tip-4'
|
||||
}, '• Keep QR code in frame')
|
||||
])
|
||||
])
|
||||
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// Export for use in other files
|
||||
window.QRScanner = QRScanner;
|
||||
console.log('QRScanner component loaded and available on window.QRScanner');
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
|
||||
|
||||
|
||||
const ComparisonTable = () => {
|
||||
const [selectedFeature, setSelectedFeature] = React.useState(null);
|
||||
|
||||
const messengers = [
|
||||
{
|
||||
name: "SecureBit.chat",
|
||||
logo: <div className="w-8 h-8 bg-orange-500/10 border border-orange-500/20 rounded-lg flex items-center justify-center">
|
||||
<i className="fas fa-shield-halved text-orange-400" />
|
||||
</div>,
|
||||
type: "P2P WebRTC",
|
||||
version: "Latest",
|
||||
color: "orange",
|
||||
},
|
||||
{
|
||||
name: "Signal",
|
||||
logo: (
|
||||
<svg className="w-8 h-8" viewBox="0 0 122.88 122.31" xmlns="http://www.w3.org/2000/svg">
|
||||
<path 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" />
|
||||
<path 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" />
|
||||
</svg>
|
||||
),
|
||||
type: "Centralized",
|
||||
version: "Latest",
|
||||
color: "blue",
|
||||
},
|
||||
{
|
||||
name: "Threema",
|
||||
logo: (
|
||||
<svg className="w-8 h-8" viewBox="0 0 122.88 122.88" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="122.88" height="122.88" rx="18.43" fill="#474747" />
|
||||
<path 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.32c21.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.64h-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.6c0-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.64h-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" />
|
||||
<circle cx="37.44" cy="97.44" r="6.72" fill="#3fe669" />
|
||||
<circle cx="61.44" cy="97.44" r="6.72" fill="#3fe669" />
|
||||
<circle cx="85.44" cy="97.44" r="6.72" fill="#3fe669" />
|
||||
</svg>
|
||||
),
|
||||
type: "Centralized",
|
||||
version: "Latest",
|
||||
color: "green",
|
||||
},
|
||||
{
|
||||
name: "Session",
|
||||
logo: (
|
||||
<svg className="w-8 h-8" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="1024" height="1024" fill="#333132" />
|
||||
<path 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" />
|
||||
</svg>
|
||||
),
|
||||
type: "Onion Network",
|
||||
version: "Latest",
|
||||
color: "cyan",
|
||||
},
|
||||
];
|
||||
|
||||
const features = [
|
||||
{
|
||||
name: "Security Architecture",
|
||||
lockbit: { status: "trophy", detail: "18-layer military-grade defense system with complete ASN.1 validation" },
|
||||
signal: { status: "check", detail: "Signal Protocol with double ratchet" },
|
||||
threema: { status: "check", detail: "Standard security implementation" },
|
||||
session: { status: "check", detail: "Modified Signal Protocol + Onion routing" },
|
||||
},
|
||||
{
|
||||
name: "Cryptography",
|
||||
lockbit: { status: "trophy", detail: "ECDH P-384 + AES-GCM 256 + ECDSA P-384" },
|
||||
signal: { status: "check", detail: "Signal Protocol + Double Ratchet" },
|
||||
threema: { status: "check", detail: "NaCl + XSalsa20 + Poly1305" },
|
||||
session: { status: "check", detail: "Modified Signal Protocol" },
|
||||
},
|
||||
{
|
||||
name: "Perfect Forward Secrecy",
|
||||
lockbit: { status: "trophy", detail: "Auto rotation every 5 minutes or 100 messages" },
|
||||
signal: { status: "check", detail: "Double Ratchet algorithm" },
|
||||
threema: { status: "warning", detail: "Partial (group chats)" },
|
||||
session: { status: "check", detail: "Session Ratchet algorithm" },
|
||||
},
|
||||
{
|
||||
name: "Architecture",
|
||||
lockbit: { status: "trophy", detail: "Pure P2P WebRTC without servers" },
|
||||
signal: { status: "times", detail: "Centralized Signal servers" },
|
||||
threema: { status: "times", detail: "Threema servers in Switzerland" },
|
||||
session: { status: "warning", detail: "Onion routing via network nodes" },
|
||||
},
|
||||
{
|
||||
name: "Registration Anonymity",
|
||||
lockbit: { status: "trophy", detail: "No registration required, instant anonymous channels" },
|
||||
signal: { status: "times", detail: "Phone number required" },
|
||||
threema: { status: "check", detail: "ID generated locally" },
|
||||
session: { status: "check", detail: "Random session ID" },
|
||||
},
|
||||
{
|
||||
name: "Payment Integration",
|
||||
lockbit: { status: "trophy", detail: "Lightning Network satoshis per session + WebLN" },
|
||||
signal: { status: "times", detail: "No payment system" },
|
||||
threema: { status: "times", detail: "No payment system" },
|
||||
session: { status: "times", detail: "No payment system" },
|
||||
},
|
||||
{
|
||||
name: "Metadata Protection",
|
||||
lockbit: { status: "trophy", detail: "Full metadata encryption + traffic obfuscation" },
|
||||
signal: { status: "warning", detail: "Sealed Sender (partial)" },
|
||||
threema: { status: "warning", detail: "Minimal metadata" },
|
||||
session: { status: "check", detail: "Onion routing hides metadata" },
|
||||
},
|
||||
{
|
||||
name: "Traffic Obfuscation",
|
||||
lockbit: { status: "trophy", detail: "Fake traffic + pattern masking + packet padding" },
|
||||
signal: { status: "times", detail: "No traffic obfuscation" },
|
||||
threema: { status: "times", detail: "No traffic obfuscation" },
|
||||
session: { status: "check", detail: "Onion routing provides obfuscation" },
|
||||
},
|
||||
{
|
||||
name: "Open Source",
|
||||
lockbit: { status: "trophy", detail: "100% open + auditable + MIT license" },
|
||||
signal: { status: "check", detail: "Fully open" },
|
||||
threema: { status: "warning", detail: "Only clients open" },
|
||||
session: { status: "check", detail: "Fully open" },
|
||||
},
|
||||
{
|
||||
name: "MITM Protection",
|
||||
lockbit: { status: "trophy", detail: "Out-of-band verification + mutual auth + ECDSA" },
|
||||
signal: { status: "check", detail: "Safety numbers verification" },
|
||||
threema: { status: "check", detail: "QR code scanning" },
|
||||
session: { status: "warning", detail: "Basic key verification" },
|
||||
},
|
||||
{
|
||||
name: "Economic Model",
|
||||
lockbit: { status: "trophy", detail: "Sustainable pay-per-session model" },
|
||||
signal: { status: "warning", detail: "Donations and grants dependency" },
|
||||
threema: { status: "check", detail: "One-time app purchase" },
|
||||
session: { status: "warning", detail: "Donations dependency" },
|
||||
},
|
||||
{
|
||||
name: "Censorship Resistance",
|
||||
lockbit: { status: "trophy", detail: "Impossible to block P2P + no servers to target" },
|
||||
signal: { status: "warning", detail: "Blocked in authoritarian countries" },
|
||||
threema: { status: "warning", detail: "May be blocked" },
|
||||
session: { status: "check", detail: "Onion routing bypasses blocks" },
|
||||
},
|
||||
{
|
||||
name: "Data Storage",
|
||||
lockbit: { status: "trophy", detail: "Zero data storage - only in browser memory" },
|
||||
signal: { status: "warning", detail: "Local database storage" },
|
||||
threema: { status: "warning", detail: "Local + optional backup" },
|
||||
session: { status: "warning", detail: "Local database storage" },
|
||||
},
|
||||
{
|
||||
name: "Key Security",
|
||||
lockbit: { status: "trophy", detail: "Non-extractable keys + hardware protection" },
|
||||
signal: { status: "check", detail: "Secure key storage" },
|
||||
threema: { status: "check", detail: "Local key storage" },
|
||||
session: { status: "check", detail: "Secure key storage" },
|
||||
},
|
||||
{
|
||||
name: "Post-Quantum Roadmap",
|
||||
lockbit: { status: "check", detail: "Planned v5.0 - CRYSTALS-Kyber/Dilithium" },
|
||||
signal: { status: "warning", detail: "PQXDH in development" },
|
||||
threema: { status: "times", detail: "Not announced" },
|
||||
session: { status: "times", detail: "Not announced" },
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
const statusMap = {
|
||||
"trophy": { icon: "fa-trophy", color: "accent-orange" },
|
||||
"check": { icon: "fa-check", color: "text-green-300" },
|
||||
"warning": { icon: "fa-exclamation-triangle", color: "text-yellow-300" },
|
||||
"times": { icon: "fa-times", color: "text-red-300" },
|
||||
};
|
||||
return statusMap[status] || { icon: "fa-question", color: "text-gray-400" };
|
||||
};
|
||||
|
||||
const toggleFeatureDetail = (index) => {
|
||||
setSelectedFeature(selectedFeature === index ? null : index);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-16">
|
||||
{/* Title */}
|
||||
<div className="text-center mb-8">
|
||||
<h3 className="text-3xl font-bold text-white mb-3">
|
||||
Enhanced Security Edition Comparison
|
||||
</h3>
|
||||
<p className="text-gray-400 max-w-2xl mx-auto mb-4">
|
||||
Enhanced Security Edition vs leading secure messengers
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Table container */}
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Mobile Alert */}
|
||||
<div className="md:hidden p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg mb-4">
|
||||
<p className="text-yellow-400 text-sm text-center">
|
||||
<i className="fas fa-lightbulb mr-2"></i>
|
||||
Rotate your device horizontally for better viewing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table
|
||||
className="w-full border-collapse rounded-xl overflow-hidden shadow-2xl"
|
||||
style={{ backgroundColor: "rgba(42, 43, 42, 0.9)" }}
|
||||
>
|
||||
{/* Table Header */}
|
||||
<thead>
|
||||
<tr className="bg-black-table">
|
||||
<th className="text-left p-4 border-b border-gray-600 text-white font-bold min-w-[240px]">
|
||||
Security Criterion
|
||||
</th>
|
||||
{messengers.map((messenger, index) => (
|
||||
<th key={`messenger-${index}`} className="text-center p-4 border-b border-gray-600 min-w-[160px]">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mb-2">{messenger.logo}</div>
|
||||
<div className={`text-sm font-bold ${
|
||||
messenger.color === 'orange' ? 'text-orange-400' :
|
||||
messenger.color === 'blue' ? 'text-blue-400' :
|
||||
messenger.color === 'green' ? 'text-green-400' :
|
||||
'text-cyan-400'
|
||||
}`}>
|
||||
{messenger.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400">{messenger.type}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{messenger.version}</div>
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/* Table body */}
|
||||
<tbody>
|
||||
{features.map((feature, featureIndex) => (
|
||||
<React.Fragment key={`feature-${featureIndex}`}>
|
||||
<tr
|
||||
className={`border-b border-gray-700/30 transition-all duration-200 cursor-pointer hover:bg-[rgb(20_20_20_/30%)] ${
|
||||
selectedFeature === featureIndex ? 'bg-[rgb(20_20_20_/50%)]' : ''
|
||||
}`}
|
||||
onClick={() => toggleFeatureDetail(featureIndex)}
|
||||
>
|
||||
<td className="p-4 text-white font-semibold">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{feature.name}</span>
|
||||
<i className={`fas fa-chevron-${selectedFeature === featureIndex ? 'up' : 'down'} text-xs text-gray-400 opacity-60 transition-all duration-200`} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<i className={`fas ${getStatusIcon(feature.lockbit.status).icon} ${getStatusIcon(feature.lockbit.status).color} text-2xl`} />
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<i className={`fas ${getStatusIcon(feature.signal.status).icon} ${getStatusIcon(feature.signal.status).color} text-2xl`} />
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<i className={`fas ${getStatusIcon(feature.threema.status).icon} ${getStatusIcon(feature.threema.status).color} text-2xl`} />
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<i className={`fas ${getStatusIcon(feature.session.status).icon} ${getStatusIcon(feature.session.status).color} text-2xl`} />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Details */}
|
||||
{selectedFeature === featureIndex && (
|
||||
<tr className="border-b border-gray-700/30 bg-gradient-to-r from-gray-800/20 to-gray-900/20">
|
||||
<td className="p-4 text-xs text-gray-400 font-medium">Technical Details:</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="text-xs text-orange-300 font-medium leading-relaxed">
|
||||
{feature.lockbit.detail}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="text-xs text-blue-300 leading-relaxed">
|
||||
{feature.signal.detail}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="text-xs text-green-300 leading-relaxed">
|
||||
{feature.threema.detail}
|
||||
</div>
|
||||
</td>
|
||||
<td className="p-4 text-center">
|
||||
<div className="text-xs text-cyan-300 leading-relaxed">
|
||||
{feature.session.detail}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-8 grid grid-cols-2 md:grid-cols-4 gap-4 max-w-5xl mx-auto">
|
||||
<div className="flex items-center justify-center p-4 bg-orange-500/10 rounded-xl hover:bg-orange-500/40 transition-colors">
|
||||
<i className="fas fa-trophy text-orange-400 mr-2 text-xl"></i>
|
||||
<span className="text-orange-300 text-sm font-bold">Category Leader</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center p-4 bg-green-500/10 rounded-xl hover:bg-green-600/40 transition-colors">
|
||||
<i className="fas fa-check text-green-300 mr-2 text-xl"></i>
|
||||
<span className="text-green-200 text-sm font-bold">Excellent</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-4 bg-yellow-500/10 rounded-xl hover:bg-yellow-600/40 transition-colors">
|
||||
<i className="fas fa-exclamation-triangle text-yellow-300 mr-2 text-xl"></i>
|
||||
<span className="text-yellow-200 text-sm font-bold">Partial/Limited</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center p-4 bg-red-500/10 rounded-xl hover:bg-red-600/40 transition-colors">
|
||||
<i className="fas fa-times text-red-300 mr-2 text-xl"></i>
|
||||
<span className="text-red-200 text-sm font-bold">Not Available</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
window.ComparisonTable = ComparisonTable;
|
||||
@@ -0,0 +1,86 @@
|
||||
const DownloadApps = () => {
|
||||
const apps = [
|
||||
{ id: 'web', name: 'Web App', subtitle: 'Browser Version', icon: 'fas fa-globe', platform: 'Web', isActive: true, url: 'https://securebit.chat/', color: 'green' },
|
||||
{ id: 'windows', name: 'Windows', subtitle: 'Desktop App', icon: 'fab fa-windows', platform: 'Desktop', isActive: false, url: 'https://securebit.chat/download/windows/SecureBit%20Chat%20Setup%204.1.222.exe', color: 'blue' },
|
||||
{ id: 'macos', name: 'macOS', subtitle: 'Desktop App', icon: 'fab fa-safari', platform: 'Desktop', isActive: false, url: '#', color: 'gray' },
|
||||
{ id: 'linux', name: 'Linux', subtitle: 'Desktop App', icon: 'fab fa-linux', platform: 'Desktop', isActive: false, url: '#', color: 'orange' },
|
||||
{ id: 'ios', name: 'iOS', subtitle: 'iPhone & iPad', icon: 'fab fa-apple', platform: 'Mobile', isActive: false, url: 'https://apps.apple.com/app/securebit-chat/', color: 'white' },
|
||||
{ id: 'android', name: 'Android', subtitle: 'Google Play', icon: 'fab fa-android', platform: 'Mobile', isActive: false, url: 'https://play.google.com/store/apps/details?id=com.securebit.chat', color: 'green' },
|
||||
{ id: 'chrome', name: 'Chrome', subtitle: 'Browser Extension', icon: 'fab fa-chrome', platform: 'Browser', isActive: false, url: '#', color: 'yellow' },
|
||||
{ id: 'edge', name: 'Edge', subtitle: 'Browser Extension', icon: 'fab fa-edge', platform: 'Browser', isActive: false, url: '#', color: 'blue' },
|
||||
{ id: 'opera', name: 'Opera', subtitle: 'Browser Extension', icon: 'fab fa-opera', platform: 'Browser', isActive: false, url: '#', color: 'red' },
|
||||
{ id: 'firefox', name: 'Firefox', subtitle: 'Browser Extension', icon: 'fab fa-firefox-browser', platform: 'Browser', isActive: false, url: '#', color: 'orange' },
|
||||
];
|
||||
|
||||
const handleDownload = (app) => {
|
||||
if (app.isActive) window.open(app.url, '_blank');
|
||||
};
|
||||
|
||||
const desktopApps = apps.filter(a => a.platform === 'Desktop' || a.platform === 'Web');
|
||||
const mobileApps = apps.filter(a => a.platform === 'Mobile');
|
||||
const browserApps = apps.filter(a => a.platform === 'Browser');
|
||||
|
||||
const cardSize = "w-28 h-28";
|
||||
|
||||
const colorClasses = {
|
||||
green: 'text-green-500',
|
||||
blue: 'text-blue-500',
|
||||
gray: 'text-gray-500',
|
||||
orange: 'text-orange-500',
|
||||
red: 'text-red-500',
|
||||
white: 'text-white',
|
||||
yellow: 'text-yellow-400',
|
||||
};
|
||||
|
||||
const renderAppCard = (app) => (
|
||||
React.createElement('div', {
|
||||
key: app.id,
|
||||
className: `group relative ${cardSize} rounded-2xl overflow-hidden card-minimal cursor-pointer`
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'bg-icon',
|
||||
className: `${app.icon} absolute text-[3rem] ${app.isActive ? colorClasses[app.color] : 'text-white/10'} top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none transition-all duration-500 group-hover:scale-105`
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'overlay',
|
||||
className: "absolute inset-0 bg-black/30 backdrop-blur-md flex flex-col items-center justify-center text-center opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
}, [
|
||||
React.createElement('h4', { key: 'name', className: `text-sm font-semibold text-primary mb-1` }, app.name),
|
||||
React.createElement('p', { key: 'subtitle', className: `text-xs text-secondary mb-2` }, app.subtitle),
|
||||
app.isActive ?
|
||||
React.createElement('button', {
|
||||
key: 'btn',
|
||||
onClick: () => handleDownload(app),
|
||||
className: `px-2 py-1 rounded-xl bg-emerald-500 text-black font-medium hover:bg-emerald-600 transition-colors text-xs`
|
||||
}, app.id === "web" ? "Launch" : "Download")
|
||||
:
|
||||
React.createElement('span', { key: 'coming', className: "text-gray-400 font-medium text-xs" }, "Coming Soon")
|
||||
])
|
||||
])
|
||||
);
|
||||
|
||||
return React.createElement('div', { className: "mt-20 px-6" }, [
|
||||
// Header
|
||||
React.createElement('div', { key: 'header', className: "text-center max-w-3xl mx-auto mb-12" }, [
|
||||
React.createElement('h3', { key: 'title', className: "text-3xl font-bold text-primary mb-3" }, 'Download SecureBit.chat'),
|
||||
React.createElement('p', { key: 'subtitle', className: "text-secondary text-lg mb-5" }, 'Stay secure on every device. Choose your platform and start chatting privately.')
|
||||
]),
|
||||
|
||||
// Desktop Apps
|
||||
React.createElement('div', { key: 'desktop-row', className: "hidden sm:flex justify-center flex-wrap gap-6 mb-6" },
|
||||
desktopApps.map(renderAppCard)
|
||||
),
|
||||
|
||||
// Mobile Apps
|
||||
React.createElement('div', { key: 'mobile-row', className: "flex justify-center gap-6 mb-6" },
|
||||
mobileApps.map(renderAppCard)
|
||||
),
|
||||
|
||||
// Browser Extensions
|
||||
React.createElement('div', { key: 'browser-row', className: "flex justify-center gap-6" },
|
||||
browserApps.map(renderAppCard)
|
||||
)
|
||||
]);
|
||||
};
|
||||
|
||||
window.DownloadApps = DownloadApps;
|
||||
@@ -0,0 +1,423 @@
|
||||
// File Transfer Component for Chat Interface - Fixed Version
|
||||
const FileTransferComponent = ({ webrtcManager, isConnected }) => {
|
||||
const [dragOver, setDragOver] = React.useState(false);
|
||||
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
|
||||
const [readyFiles, setReadyFiles] = React.useState([]); // файлы, готовые к скачиванию
|
||||
const fileInputRef = React.useRef(null);
|
||||
|
||||
// Update transfers periodically
|
||||
React.useEffect(() => {
|
||||
if (!isConnected || !webrtcManager) return;
|
||||
|
||||
const updateTransfers = () => {
|
||||
const currentTransfers = webrtcManager.getFileTransfers();
|
||||
setTransfers(currentTransfers);
|
||||
};
|
||||
|
||||
const interval = setInterval(updateTransfers, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, [isConnected, webrtcManager]);
|
||||
|
||||
// Setup file transfer callbacks - ИСПРАВЛЕНИЕ: НЕ отправляем промежуточные сообщения в чат
|
||||
React.useEffect(() => {
|
||||
if (!webrtcManager) return;
|
||||
|
||||
webrtcManager.setFileTransferCallbacks(
|
||||
// Progress callback - ТОЛЬКО обновляем UI, НЕ отправляем в чат
|
||||
(progress) => {
|
||||
// Обновляем только локальное состояние
|
||||
const currentTransfers = webrtcManager.getFileTransfers();
|
||||
setTransfers(currentTransfers);
|
||||
|
||||
// НЕ отправляем сообщения в чат!
|
||||
},
|
||||
|
||||
// File received callback - добавляем кнопку скачивания в UI
|
||||
(fileData) => {
|
||||
// Добавляем в список готовых к скачиванию
|
||||
setReadyFiles(prev => {
|
||||
// избегаем дублей по fileId
|
||||
if (prev.some(f => f.fileId === fileData.fileId)) return prev;
|
||||
return [...prev, {
|
||||
fileId: fileData.fileId,
|
||||
fileName: fileData.fileName,
|
||||
fileSize: fileData.fileSize,
|
||||
mimeType: fileData.mimeType,
|
||||
getBlob: fileData.getBlob,
|
||||
getObjectURL: fileData.getObjectURL,
|
||||
revokeObjectURL: fileData.revokeObjectURL
|
||||
}];
|
||||
});
|
||||
|
||||
// Обновляем список активных передач
|
||||
const currentTransfers = webrtcManager.getFileTransfers();
|
||||
setTransfers(currentTransfers);
|
||||
},
|
||||
|
||||
// Error callback
|
||||
(error) => {
|
||||
const currentTransfers = webrtcManager.getFileTransfers();
|
||||
setTransfers(currentTransfers);
|
||||
|
||||
// ИСПРАВЛЕНИЕ: НЕ дублируем сообщения об ошибках
|
||||
// Уведомления об ошибках уже отправляются в WebRTC менеджере
|
||||
}
|
||||
);
|
||||
}, [webrtcManager]);
|
||||
|
||||
const handleFileSelect = async (files) => {
|
||||
if (!isConnected || !webrtcManager) {
|
||||
alert('Соединение не установлено. Сначала установите соединение.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Дополнительная проверка состояния соединения
|
||||
if (!webrtcManager.isConnected() || !webrtcManager.isVerified) {
|
||||
alert('Соединение не готово для передачи файлов. Дождитесь завершения установки соединения.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Валидация файла перед отправкой
|
||||
const validation = webrtcManager.validateFile(file);
|
||||
if (!validation.isValid) {
|
||||
const errorMessage = validation.errors.join('. ');
|
||||
alert(`Файл ${file.name} не может быть отправлен: ${errorMessage}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await webrtcManager.sendFile(file);
|
||||
} catch (error) {
|
||||
// Более мягкая обработка ошибок - не закрываем сессию
|
||||
|
||||
// Показываем пользователю ошибку, но не закрываем соединение
|
||||
if (error.message.includes('Connection not ready')) {
|
||||
alert(`Файл ${file.name} не может быть отправлен сейчас. Проверьте соединение и попробуйте снова.`);
|
||||
} else if (error.message.includes('File too large') || error.message.includes('exceeds maximum')) {
|
||||
alert(`Файл ${file.name} слишком большой: ${error.message}`);
|
||||
} else if (error.message.includes('Maximum concurrent transfers')) {
|
||||
alert(`Достигнут лимит одновременных передач. Дождитесь завершения текущих передач.`);
|
||||
} else if (error.message.includes('File type not allowed')) {
|
||||
alert(`Тип файла ${file.name} не поддерживается: ${error.message}`);
|
||||
} else {
|
||||
alert(`Ошибка отправки файла ${file.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
handleFileSelect(files);
|
||||
};
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
setDragOver(false);
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e) => {
|
||||
const files = Array.from(e.target.files);
|
||||
handleFileSelect(files);
|
||||
e.target.value = ''; // Reset input
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const getStatusIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'metadata_sent':
|
||||
case 'preparing':
|
||||
return 'fas fa-cog fa-spin';
|
||||
case 'transmitting':
|
||||
case 'receiving':
|
||||
return 'fas fa-exchange-alt fa-pulse';
|
||||
case 'assembling':
|
||||
return 'fas fa-puzzle-piece fa-pulse';
|
||||
case 'completed':
|
||||
return 'fas fa-check text-green-400';
|
||||
case 'failed':
|
||||
return 'fas fa-times text-red-400';
|
||||
default:
|
||||
return 'fas fa-circle';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusText = (status) => {
|
||||
switch (status) {
|
||||
case 'metadata_sent':
|
||||
return 'Подготовка...';
|
||||
case 'transmitting':
|
||||
return 'Отправка...';
|
||||
case 'receiving':
|
||||
return 'Получение...';
|
||||
case 'assembling':
|
||||
return 'Сборка файла...';
|
||||
case 'completed':
|
||||
return 'Завершено';
|
||||
case 'failed':
|
||||
return 'Ошибка';
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
if (!isConnected) {
|
||||
return React.createElement('div', {
|
||||
className: "p-4 text-center text-muted"
|
||||
}, 'Передача файлов доступна только при установленном соединении');
|
||||
}
|
||||
|
||||
// Проверяем дополнительное состояние соединения
|
||||
const isConnectionReady = webrtcManager && webrtcManager.isConnected() && webrtcManager.isVerified;
|
||||
|
||||
if (!isConnectionReady) {
|
||||
return React.createElement('div', {
|
||||
className: "p-4 text-center text-yellow-600"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exclamation-triangle mr-2'
|
||||
}),
|
||||
'Соединение устанавливается... Передача файлов будет доступна после завершения установки.'
|
||||
]);
|
||||
}
|
||||
|
||||
return React.createElement('div', {
|
||||
className: "file-transfer-component"
|
||||
}, [
|
||||
// File Drop Zone
|
||||
React.createElement('div', {
|
||||
key: 'drop-zone',
|
||||
className: `file-drop-zone ${dragOver ? 'drag-over' : ''}`,
|
||||
onDrop: handleDrop,
|
||||
onDragOver: handleDragOver,
|
||||
onDragLeave: handleDragLeave,
|
||||
onClick: () => fileInputRef.current?.click()
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'drop-content',
|
||||
className: "drop-content"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-cloud-upload-alt text-2xl mb-2 text-blue-400'
|
||||
}),
|
||||
React.createElement('p', {
|
||||
key: 'text',
|
||||
className: "text-primary font-medium"
|
||||
}, 'Drag files here or click to select'),
|
||||
React.createElement('p', {
|
||||
key: 'subtext',
|
||||
className: "text-muted text-sm"
|
||||
}, 'Maximum size: 100 MB per file')
|
||||
])
|
||||
]),
|
||||
|
||||
// Hidden file input
|
||||
React.createElement('input', {
|
||||
key: 'file-input',
|
||||
ref: fileInputRef,
|
||||
type: 'file',
|
||||
multiple: true,
|
||||
className: 'hidden',
|
||||
onChange: handleFileInputChange
|
||||
}),
|
||||
|
||||
// Active Transfers
|
||||
(transfers.sending.length > 0 || transfers.receiving.length > 0) && React.createElement('div', {
|
||||
key: 'transfers',
|
||||
className: "active-transfers mt-4"
|
||||
}, [
|
||||
React.createElement('h4', {
|
||||
key: 'title',
|
||||
className: "text-primary font-medium mb-3 flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exchange-alt mr-2'
|
||||
}),
|
||||
'Передача файлов'
|
||||
]),
|
||||
|
||||
// Sending files
|
||||
...transfers.sending.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `send-${transfer.fileId}`,
|
||||
className: "transfer-item bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 mb-2"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: "flex items-center justify-between mb-2"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'info',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-upload text-blue-400 mr-2'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "text-primary font-medium text-sm"
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-muted text-xs ml-2"
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'cancel',
|
||||
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
|
||||
className: "text-red-400 hover:text-red-300 text-xs"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times'
|
||||
})
|
||||
])
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: "progress-bar"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'fill',
|
||||
className: "progress-fill bg-blue-400",
|
||||
style: { width: `${transfer.progress}%` }
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'text',
|
||||
className: "progress-text text-xs flex items-center justify-between"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${getStatusIcon(transfer.status)} mr-1`
|
||||
}),
|
||||
getStatusText(transfer.status)
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'percent'
|
||||
}, `${transfer.progress.toFixed(1)}%`)
|
||||
])
|
||||
])
|
||||
])
|
||||
),
|
||||
|
||||
// Receiving files
|
||||
...transfers.receiving.map(transfer =>
|
||||
React.createElement('div', {
|
||||
key: `recv-${transfer.fileId}`,
|
||||
className: "transfer-item bg-green-500/10 border border-green-500/20 rounded-lg p-3 mb-2"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: "flex items-center justify-between mb-2"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'info',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-download text-green-400 mr-2'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'name',
|
||||
className: "text-primary font-medium text-sm"
|
||||
}, transfer.fileName),
|
||||
React.createElement('span', {
|
||||
key: 'size',
|
||||
className: "text-muted text-xs ml-2"
|
||||
}, formatFileSize(transfer.fileSize))
|
||||
]),
|
||||
React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [
|
||||
(() => {
|
||||
const rf = readyFiles.find(f => f.fileId === transfer.fileId);
|
||||
if (!rf || transfer.status !== 'completed') return null;
|
||||
return React.createElement('button', {
|
||||
key: 'download',
|
||||
className: 'text-green-400 hover:text-green-300 text-xs flex items-center',
|
||||
onClick: async () => {
|
||||
try {
|
||||
const url = await rf.getObjectURL();
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = rf.fileName || 'file';
|
||||
a.click();
|
||||
rf.revokeObjectURL(url);
|
||||
} catch (e) {
|
||||
alert('Failed to start download: ' + e.message);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
React.createElement('i', { key: 'i', className: 'fas fa-download mr-1' }),
|
||||
'Download'
|
||||
]);
|
||||
})(),
|
||||
React.createElement('button', {
|
||||
key: 'cancel',
|
||||
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
|
||||
className: "text-red-400 hover:text-red-300 text-xs"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times'
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: "progress-bar"
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'fill',
|
||||
className: "progress-fill bg-green-400",
|
||||
style: { width: `${transfer.progress}%` }
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'text',
|
||||
className: "progress-text text-xs flex items-center justify-between"
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
key: 'status',
|
||||
className: "flex items-center"
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${getStatusIcon(transfer.status)} mr-1`
|
||||
}),
|
||||
getStatusText(transfer.status)
|
||||
]),
|
||||
React.createElement('span', {
|
||||
key: 'percent'
|
||||
}, `${transfer.progress.toFixed(1)}%`)
|
||||
])
|
||||
])
|
||||
])
|
||||
)
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
// Export
|
||||
window.FileTransferComponent = FileTransferComponent;
|
||||
@@ -0,0 +1,708 @@
|
||||
const EnhancedMinimalHeader = ({
|
||||
status,
|
||||
fingerprint,
|
||||
verificationCode,
|
||||
onDisconnect,
|
||||
isConnected,
|
||||
securityLevel,
|
||||
sessionManager,
|
||||
sessionTimeLeft,
|
||||
webrtcManager
|
||||
}) => {
|
||||
const [currentTimeLeft, setCurrentTimeLeft] = React.useState(sessionTimeLeft || 0);
|
||||
const [hasActiveSession, setHasActiveSession] = React.useState(false);
|
||||
const [sessionType, setSessionType] = React.useState('unknown');
|
||||
const [realSecurityLevel, setRealSecurityLevel] = React.useState(null);
|
||||
const [lastSecurityUpdate, setLastSecurityUpdate] = React.useState(0);
|
||||
|
||||
// ============================================
|
||||
// FIXED SECURITY UPDATE LOGIC
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
let isUpdating = false;
|
||||
let lastUpdateAttempt = 0;
|
||||
|
||||
const updateRealSecurityStatus = async () => {
|
||||
const now = Date.now();
|
||||
if (now - lastUpdateAttempt < 10000) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdating = true;
|
||||
lastUpdateAttempt = now;
|
||||
|
||||
try {
|
||||
if (!webrtcManager || !isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeWebrtcManager = webrtcManager;
|
||||
|
||||
let realSecurityData = null;
|
||||
|
||||
if (typeof activeWebrtcManager.getRealSecurityLevel === 'function') {
|
||||
realSecurityData = await activeWebrtcManager.getRealSecurityLevel();
|
||||
} else if (typeof activeWebrtcManager.calculateAndReportSecurityLevel === 'function') {
|
||||
realSecurityData = await activeWebrtcManager.calculateAndReportSecurityLevel();
|
||||
} else {
|
||||
realSecurityData = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(activeWebrtcManager);
|
||||
}
|
||||
|
||||
if (realSecurityData && realSecurityData.isRealData !== false) {
|
||||
const currentScore = realSecurityLevel?.score || 0;
|
||||
const newScore = realSecurityData.score || 0;
|
||||
|
||||
if (currentScore !== newScore || !realSecurityLevel) {
|
||||
setRealSecurityLevel(realSecurityData);
|
||||
setLastSecurityUpdate(now);
|
||||
|
||||
} else if (window.DEBUG_MODE) {
|
||||
}
|
||||
} else {
|
||||
console.warn(' Security calculation returned invalid data');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(' Error in real security calculation:', error);
|
||||
} finally {
|
||||
isUpdating = false;
|
||||
}
|
||||
};
|
||||
|
||||
if (isConnected) {
|
||||
updateRealSecurityStatus();
|
||||
|
||||
if (!realSecurityLevel || realSecurityLevel.score < 50) {
|
||||
const retryInterval = setInterval(() => {
|
||||
if (!realSecurityLevel || realSecurityLevel.score < 50) {
|
||||
updateRealSecurityStatus();
|
||||
} else {
|
||||
clearInterval(retryInterval);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
setTimeout(() => clearInterval(retryInterval), 30000);
|
||||
}
|
||||
}
|
||||
|
||||
const interval = setInterval(updateRealSecurityStatus, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [webrtcManager, isConnected]);
|
||||
|
||||
// ============================================
|
||||
// FIXED EVENT HANDLERS
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleSecurityUpdate = (event) => {
|
||||
|
||||
setTimeout(() => {
|
||||
setLastSecurityUpdate(0);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleRealSecurityCalculated = (event) => {
|
||||
|
||||
if (event.detail && event.detail.securityData) {
|
||||
setRealSecurityLevel(event.detail.securityData);
|
||||
setLastSecurityUpdate(Date.now());
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('security-level-updated', handleSecurityUpdate);
|
||||
document.addEventListener('real-security-calculated', handleRealSecurityCalculated);
|
||||
|
||||
window.forceHeaderSecurityUpdate = (webrtcManager) => {
|
||||
|
||||
if (webrtcManager && window.EnhancedSecureCryptoUtils) {
|
||||
window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager)
|
||||
.then(securityData => {
|
||||
if (securityData && securityData.isRealData !== false) {
|
||||
setRealSecurityLevel(securityData);
|
||||
setLastSecurityUpdate(Date.now());
|
||||
console.log('✅ Header security level force-updated');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ Force update failed:', error);
|
||||
});
|
||||
} else {
|
||||
setLastSecurityUpdate(0);
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('security-level-updated', handleSecurityUpdate);
|
||||
document.removeEventListener('real-security-calculated', handleRealSecurityCalculated);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ============================================
|
||||
// REST of the component logic
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
// All security features are enabled by default - no session management needed
|
||||
setHasActiveSession(true);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('premium'); // All features enabled
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
// All security features are enabled by default
|
||||
setHasActiveSession(true);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('premium'); // All features enabled
|
||||
}, [sessionTimeLeft]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleForceUpdate = (event) => {
|
||||
// All security features are enabled by default
|
||||
setHasActiveSession(true);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('premium'); // All features enabled
|
||||
};
|
||||
|
||||
// Connection cleanup handler (use existing event from module)
|
||||
const handleConnectionCleaned = () => {
|
||||
if (window.DEBUG_MODE) {
|
||||
console.log('🧹 Connection cleaned - clearing security data in header');
|
||||
}
|
||||
|
||||
setRealSecurityLevel(null);
|
||||
setLastSecurityUpdate(0);
|
||||
|
||||
setHasActiveSession(false);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('unknown');
|
||||
};
|
||||
|
||||
const handlePeerDisconnect = () => {
|
||||
if (window.DEBUG_MODE) {
|
||||
console.log('👋 Peer disconnect detected - clearing security data in header');
|
||||
}
|
||||
|
||||
setRealSecurityLevel(null);
|
||||
setLastSecurityUpdate(0);
|
||||
};
|
||||
|
||||
const handleDisconnected = () => {
|
||||
|
||||
setRealSecurityLevel(null);
|
||||
setLastSecurityUpdate(0);
|
||||
setHasActiveSession(false);
|
||||
setCurrentTimeLeft(0);
|
||||
setSessionType('unknown');
|
||||
};
|
||||
|
||||
document.addEventListener('force-header-update', handleForceUpdate);
|
||||
document.addEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.addEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.addEventListener('disconnected', handleDisconnected);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('force-header-update', handleForceUpdate);
|
||||
document.removeEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.removeEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.removeEventListener('disconnected', handleDisconnected);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// ============================================
|
||||
// SECURITY INDICATOR CLICK HANDLER
|
||||
// ============================================
|
||||
|
||||
const handleSecurityClick = async (event) => {
|
||||
// Check if it's a right-click or Ctrl+click to disconnect
|
||||
if (event && (event.button === 2 || event.ctrlKey || event.metaKey)) {
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent default behavior
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
// Run real security tests if webrtcManager is available
|
||||
let realTestResults = null;
|
||||
if (webrtcManager && window.EnhancedSecureCryptoUtils) {
|
||||
try {
|
||||
realTestResults = await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(webrtcManager);
|
||||
console.log('✅ Real security tests completed:', realTestResults);
|
||||
} catch (error) {
|
||||
console.error('❌ Real security tests failed:', error);
|
||||
}
|
||||
} else {
|
||||
console.log('⚠️ Cannot run security tests:', {
|
||||
webrtcManager: !!webrtcManager,
|
||||
cryptoUtils: !!window.EnhancedSecureCryptoUtils
|
||||
});
|
||||
}
|
||||
|
||||
// If no real test results and no existing security level, show progress message
|
||||
if (!realTestResults && !realSecurityLevel) {
|
||||
alert('Security verification in progress...\nPlease wait for real-time cryptographic verification to complete.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use real test results if available, otherwise fall back to current data
|
||||
let securityData = realTestResults || realSecurityLevel;
|
||||
|
||||
// If still no security data, create a basic fallback
|
||||
if (!securityData) {
|
||||
securityData = {
|
||||
level: 'UNKNOWN',
|
||||
score: 0,
|
||||
color: 'gray',
|
||||
verificationResults: {},
|
||||
timestamp: Date.now(),
|
||||
details: 'Security verification not available',
|
||||
isRealData: false,
|
||||
passedChecks: 0,
|
||||
totalChecks: 0,
|
||||
sessionType: 'unknown'
|
||||
};
|
||||
console.log('Using fallback security data:', securityData);
|
||||
}
|
||||
|
||||
// Detailed information about the REAL security check
|
||||
let message = `REAL-TIME SECURITY VERIFICATION\n\n`;
|
||||
message += `Security Level: ${securityData.level} (${securityData.score}%)\n`;
|
||||
message += `Session Type: ${securityData.sessionType || 'premium'}\n`;
|
||||
message += `Verification Time: ${new Date(securityData.timestamp).toLocaleTimeString()}\n`;
|
||||
message += `Data Source: ${securityData.isRealData ? 'Real Cryptographic Tests' : 'Simulated Data'}\n\n`;
|
||||
|
||||
if (securityData.verificationResults) {
|
||||
message += 'DETAILED CRYPTOGRAPHIC TESTS:\n';
|
||||
message += '=' + '='.repeat(40) + '\n';
|
||||
|
||||
const passedTests = Object.entries(securityData.verificationResults).filter(([key, result]) => result.passed);
|
||||
const failedTests = Object.entries(securityData.verificationResults).filter(([key, result]) => !result.passed);
|
||||
|
||||
if (passedTests.length > 0) {
|
||||
message += 'PASSED TESTS:\n';
|
||||
passedTests.forEach(([key, result]) => {
|
||||
const testName = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
|
||||
message += ` ${testName}: ${result.details || 'Test passed'}\n`;
|
||||
});
|
||||
message += '\n';
|
||||
}
|
||||
|
||||
if (failedTests.length > 0) {
|
||||
message += 'FAILED/UNAVAILABLE TESTS:\n';
|
||||
failedTests.forEach(([key, result]) => {
|
||||
const testName = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
|
||||
message += ` ${testName}: ${result.details || 'Test failed or unavailable'}\n`;
|
||||
});
|
||||
message += '\n';
|
||||
}
|
||||
|
||||
message += `SUMMARY:\n`;
|
||||
message += `Passed: ${securityData.passedChecks}/${securityData.totalChecks} tests\n`;
|
||||
message += `Score: ${securityData.score}/${securityData.maxPossibleScore || 100} points\n\n`;
|
||||
}
|
||||
|
||||
// Real security features status
|
||||
message += `SECURITY FEATURES STATUS:\n`;
|
||||
message += '=' + '='.repeat(40) + '\n';
|
||||
|
||||
if (securityData.verificationResults) {
|
||||
const features = {
|
||||
'ECDSA Digital Signatures': securityData.verificationResults.verifyECDSASignatures?.passed || false,
|
||||
'ECDH Key Exchange': securityData.verificationResults.verifyECDHKeyExchange?.passed || false,
|
||||
'AES-GCM Encryption': securityData.verificationResults.verifyEncryption?.passed || false,
|
||||
'Message Integrity (HMAC)': securityData.verificationResults.verifyMessageIntegrity?.passed || false,
|
||||
'Perfect Forward Secrecy': securityData.verificationResults.verifyPerfectForwardSecrecy?.passed || false,
|
||||
'Replay Protection': securityData.verificationResults.verifyReplayProtection?.passed || false,
|
||||
'DTLS Fingerprint': securityData.verificationResults.verifyDTLSFingerprint?.passed || false,
|
||||
'SAS Verification': securityData.verificationResults.verifySASVerification?.passed || false,
|
||||
'Metadata Protection': securityData.verificationResults.verifyMetadataProtection?.passed || false,
|
||||
'Traffic Obfuscation': securityData.verificationResults.verifyTrafficObfuscation?.passed || false
|
||||
};
|
||||
|
||||
Object.entries(features).forEach(([feature, isEnabled]) => {
|
||||
message += `${isEnabled ? '✅' : '❌'} ${feature}\n`;
|
||||
});
|
||||
} else {
|
||||
// Fallback if no verification results
|
||||
message += `✅ ECDSA Digital Signatures\n`;
|
||||
message += `✅ ECDH Key Exchange\n`;
|
||||
message += `✅ AES-GCM Encryption\n`;
|
||||
message += `✅ Message Integrity (HMAC)\n`;
|
||||
message += `✅ Perfect Forward Secrecy\n`;
|
||||
message += `✅ Replay Protection\n`;
|
||||
message += `✅ DTLS Fingerprint\n`;
|
||||
message += `✅ SAS Verification\n`;
|
||||
message += `✅ Metadata Protection\n`;
|
||||
message += `✅ Traffic Obfuscation\n`;
|
||||
}
|
||||
|
||||
message += `\n${securityData.details || 'Real cryptographic verification completed'}`;
|
||||
|
||||
if (securityData.isRealData) {
|
||||
message += '\n\n✅ This is REAL-TIME verification using actual cryptographic functions.';
|
||||
} else {
|
||||
message += '\n\n⚠️ Warning: This data may be simulated. Connection may not be fully established.';
|
||||
}
|
||||
|
||||
// Show in a more user-friendly way
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.8);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: monospace;
|
||||
`;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
max-height: 80%;
|
||||
overflow-y: auto;
|
||||
white-space: pre-line;
|
||||
border: 1px solid #333;
|
||||
`;
|
||||
|
||||
content.textContent = message;
|
||||
modal.appendChild(content);
|
||||
|
||||
// Close on click outside
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
document.body.removeChild(modal);
|
||||
}
|
||||
});
|
||||
|
||||
// Close on Escape key
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(modal);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
document.body.appendChild(modal);
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// DISPLAY UTILITIES
|
||||
// ============================================
|
||||
|
||||
const getStatusConfig = () => {
|
||||
switch (status) {
|
||||
case 'connected':
|
||||
return {
|
||||
text: 'Connected',
|
||||
className: 'status-connected',
|
||||
badgeClass: 'bg-green-500/10 text-green-400 border-green-500/20'
|
||||
};
|
||||
case 'verifying':
|
||||
return {
|
||||
text: 'Verifying...',
|
||||
className: 'status-verifying',
|
||||
badgeClass: 'bg-purple-500/10 text-purple-400 border-purple-500/20'
|
||||
};
|
||||
case 'connecting':
|
||||
return {
|
||||
text: 'Connecting...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-blue-500/10 text-blue-400 border-blue-500/20'
|
||||
};
|
||||
case 'retrying':
|
||||
return {
|
||||
text: 'Retrying...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
text: 'Error',
|
||||
className: 'status-failed',
|
||||
badgeClass: 'bg-red-500/10 text-red-400 border-red-500/20'
|
||||
};
|
||||
case 'reconnecting':
|
||||
return {
|
||||
text: 'Reconnecting...',
|
||||
className: 'status-connecting',
|
||||
badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20'
|
||||
};
|
||||
case 'peer_disconnected':
|
||||
return {
|
||||
text: 'Peer disconnected',
|
||||
className: 'status-failed',
|
||||
badgeClass: 'bg-orange-500/10 text-orange-400 border-orange-500/20'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
text: 'Not connected',
|
||||
className: 'status-disconnected',
|
||||
badgeClass: 'bg-gray-500/10 text-gray-400 border-gray-500/20'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const config = getStatusConfig();
|
||||
const displaySecurityLevel = isConnected ? (realSecurityLevel || securityLevel) : null;
|
||||
|
||||
const shouldShowTimer = hasActiveSession && currentTimeLeft > 0 && window.SessionTimer;
|
||||
|
||||
// ============================================
|
||||
// DATA RELIABILITY INDICATOR
|
||||
// ============================================
|
||||
|
||||
const getSecurityIndicatorDetails = () => {
|
||||
if (!displaySecurityLevel) {
|
||||
return {
|
||||
tooltip: 'Security verification in progress...',
|
||||
isVerified: false,
|
||||
dataSource: 'loading'
|
||||
};
|
||||
}
|
||||
|
||||
const isRealData = displaySecurityLevel.isRealData !== false;
|
||||
const baseTooltip = `${displaySecurityLevel.level} (${displaySecurityLevel.score}%)`;
|
||||
|
||||
if (isRealData) {
|
||||
return {
|
||||
tooltip: `${baseTooltip} - Real-time verification ✅\nRight-click or Ctrl+click to disconnect`,
|
||||
isVerified: true,
|
||||
dataSource: 'real'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
tooltip: `${baseTooltip} - Estimated (connection establishing...)\nRight-click or Ctrl+click to disconnect`,
|
||||
isVerified: false,
|
||||
dataSource: 'estimated'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const securityDetails = getSecurityIndicatorDetails();
|
||||
|
||||
// ============================================
|
||||
// ADDING global methods for debugging
|
||||
// ============================================
|
||||
|
||||
React.useEffect(() => {
|
||||
window.debugHeaderSecurity = () => {
|
||||
console.log('🔍 Header Security Debug:', {
|
||||
realSecurityLevel,
|
||||
lastSecurityUpdate,
|
||||
isConnected,
|
||||
webrtcManagerProp: !!webrtcManager,
|
||||
windowWebrtcManager: !!window.webrtcManager,
|
||||
cryptoUtils: !!window.EnhancedSecureCryptoUtils,
|
||||
displaySecurityLevel: displaySecurityLevel,
|
||||
securityDetails: securityDetails
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
delete window.debugHeaderSecurity;
|
||||
};
|
||||
}, [realSecurityLevel, lastSecurityUpdate, isConnected, webrtcManager, displaySecurityLevel, securityDetails]);
|
||||
|
||||
// ============================================
|
||||
// RENDER
|
||||
// ============================================
|
||||
|
||||
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
|
||||
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'
|
||||
}, 'SecureBit.chat'),
|
||||
React.createElement('p', {
|
||||
key: 'subtitle',
|
||||
className: 'text-xs sm:text-sm text-muted hidden sm:block'
|
||||
}, 'End-to-end freedom v4.3.120')
|
||||
])
|
||||
]),
|
||||
|
||||
// Status and Controls - Responsive
|
||||
React.createElement('div', {
|
||||
key: 'status-section',
|
||||
className: 'flex items-center space-x-2 sm:space-x-3'
|
||||
}, [
|
||||
// Session Timer - all features enabled by default
|
||||
shouldShowTimer && React.createElement(window.SessionTimer, {
|
||||
key: 'session-timer',
|
||||
timeLeft: currentTimeLeft,
|
||||
sessionType: sessionType,
|
||||
onDisconnect: onDisconnect
|
||||
}),
|
||||
|
||||
displaySecurityLevel && 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: handleSecurityClick,
|
||||
onContextMenu: (e) => {
|
||||
e.preventDefault();
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
},
|
||||
title: securityDetails.tooltip
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'security-icon',
|
||||
className: `w-6 h-6 rounded-full flex items-center justify-center relative ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-500/20' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-500/20' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
||||
} ${securityDetails.isVerified ? '' : 'animate-pulse'}`
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: `fas fa-shield-alt text-xs ${
|
||||
displaySecurityLevel.color === 'green' ? 'text-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'text-orange-400' :
|
||||
displaySecurityLevel.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 flex items-center space-x-1'
|
||||
}, [
|
||||
React.createElement('span', {}, `${displaySecurityLevel.level} (${displaySecurityLevel.score}%)`)
|
||||
]),
|
||||
React.createElement('div', {
|
||||
key: 'security-details',
|
||||
className: 'text-xs text-muted mt-1 hidden lg:block'
|
||||
}, securityDetails.dataSource === 'real' ?
|
||||
`${displaySecurityLevel.passedChecks || 0}/${displaySecurityLevel.totalChecks || 0} tests` :
|
||||
(displaySecurityLevel.details || `Stage ${displaySecurityLevel.stage || 1}`)
|
||||
),
|
||||
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 ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-400' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-400' : 'bg-red-400'
|
||||
}`,
|
||||
style: { width: `${displaySecurityLevel.score}%` }
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Mobile Security Indicator
|
||||
displaySecurityLevel && 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 relative ${
|
||||
displaySecurityLevel.color === 'green' ? 'bg-green-500/20' :
|
||||
displaySecurityLevel.color === 'orange' ? 'bg-orange-500/20' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20'
|
||||
} ${securityDetails.isVerified ? '' : 'animate-pulse'}`,
|
||||
title: securityDetails.tooltip,
|
||||
onClick: handleSecurityClick,
|
||||
onContextMenu: (e) => {
|
||||
e.preventDefault();
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
}
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: `fas fa-shield-alt text-sm ${
|
||||
displaySecurityLevel.color === 'green' ? 'text-green-400' :
|
||||
displaySecurityLevel.color === 'orange' ? 'text-orange-400' :
|
||||
displaySecurityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400'
|
||||
}`
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Status Badge
|
||||
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
|
||||
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', {
|
||||
className: 'fas fa-power-off sm:mr-2'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
className: 'hidden sm:inline'
|
||||
}, 'Disconnect')
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.EnhancedMinimalHeader = EnhancedMinimalHeader;
|
||||
@@ -0,0 +1,91 @@
|
||||
const React = window.React;
|
||||
|
||||
const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPassword }) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (password.trim()) {
|
||||
onSubmit(password.trim());
|
||||
setPassword('');
|
||||
}
|
||||
};
|
||||
|
||||
const getActionText = () => {
|
||||
return action === 'offer' ? 'invitation' : 'response';
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'modal',
|
||||
className: 'card-minimal rounded-xl p-6 max-w-md w-full 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-key accent-purple'
|
||||
})
|
||||
]),
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: 'text-lg font-medium text-primary'
|
||||
}, 'Password input')
|
||||
]),
|
||||
React.createElement('form', {
|
||||
key: 'form',
|
||||
onSubmit: handleSubmit,
|
||||
className: 'space-y-4'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
key: 'description',
|
||||
className: 'text-secondary text-sm'
|
||||
}, `Enter password for decryption ${getActionText()}:`),
|
||||
React.createElement('input', {
|
||||
key: 'password-input',
|
||||
type: 'password',
|
||||
value: password,
|
||||
onChange: (e) => setPassword(e.target.value),
|
||||
placeholder: 'Enter password...',
|
||||
className: 'w-full p-3 bg-gray-900/30 border border-gray-500/20 rounded-lg text-primary placeholder-gray-500 focus:border-purple-500/40 focus:outline-none transition-all',
|
||||
autoFocus: true
|
||||
}),
|
||||
React.createElement('div', {
|
||||
key: 'buttons',
|
||||
className: 'flex space-x-3'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'submit',
|
||||
type: 'submit',
|
||||
className: 'flex-1 btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-unlock-alt mr-2'
|
||||
}),
|
||||
'Decrypt'
|
||||
]),
|
||||
React.createElement('button', {
|
||||
key: 'cancel',
|
||||
type: 'button',
|
||||
onClick: onClose,
|
||||
className: 'flex-1 btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
className: 'fas fa-times mr-2'
|
||||
}),
|
||||
'Cancel'
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.PasswordModal = PasswordModal;
|
||||
@@ -0,0 +1,432 @@
|
||||
function Roadmap() {
|
||||
const [selectedPhase, setSelectedPhase] = React.useState(null);
|
||||
const phases = [
|
||||
{
|
||||
version: "v1.0",
|
||||
title: "Start of Development",
|
||||
status: "done",
|
||||
date: "Early 2025",
|
||||
description: "Idea, prototype, and infrastructure setup",
|
||||
features: [
|
||||
"Concept and requirements formation",
|
||||
"Stack selection: WebRTC, P2P, cryptography",
|
||||
"First messaging prototypes",
|
||||
"Repository creation and CI",
|
||||
"Basic encryption architecture",
|
||||
"UX/UI design"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v1.5",
|
||||
title: "Alpha Release",
|
||||
status: "done",
|
||||
date: "Spring 2025",
|
||||
description: "First public alpha: basic chat and key exchange",
|
||||
features: [
|
||||
"Basic P2P messaging via WebRTC",
|
||||
"Simple E2E encryption (demo scheme)",
|
||||
"Stable signaling and reconnection",
|
||||
"Minimal UX for testing",
|
||||
"Feedback collection from early testers"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v2.0",
|
||||
title: "Security Hardened",
|
||||
status: "done",
|
||||
date: "Summer 2025",
|
||||
description: "Security strengthening and stable branch release",
|
||||
features: [
|
||||
"ECDH/ECDSA implementation in production",
|
||||
"Perfect Forward Secrecy and key rotation",
|
||||
"Improved authentication checks",
|
||||
"File encryption and large payload transfers",
|
||||
"Audit of basic cryptoprocesses"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v3.0",
|
||||
title: "Scaling & Stability",
|
||||
status: "done",
|
||||
date: "Fall 2025",
|
||||
description: "Network scaling and stability improvements",
|
||||
features: [
|
||||
"Optimization of P2P connections and NAT traversal",
|
||||
"Reconnection mechanisms and message queues",
|
||||
"Reduced battery consumption on mobile",
|
||||
"Support for multi-device synchronization",
|
||||
"Monitoring and logging tools for developers"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v3.5",
|
||||
title: "Privacy-first Release",
|
||||
status: "done",
|
||||
date: "Winter 2025",
|
||||
description: "Focus on privacy: minimizing metadata",
|
||||
features: [
|
||||
"Metadata protection and fingerprint reduction",
|
||||
"Experiments with onion routing and DHT",
|
||||
"Options for anonymous connections",
|
||||
"Preparation for open code audit",
|
||||
"Improved user verification processes"
|
||||
]
|
||||
},
|
||||
|
||||
// current and future phases
|
||||
{
|
||||
version: "v4.3.120",
|
||||
title: "Enhanced Security Edition",
|
||||
status: "current",
|
||||
date: "Now",
|
||||
description: "Current version with ECDH + DTLS + SAS security, 18-layer military-grade cryptography and complete ASN.1 validation",
|
||||
features: [
|
||||
"ECDH + DTLS + SAS triple-layer security",
|
||||
"ECDH P-384 + AES-GCM 256-bit encryption",
|
||||
"DTLS fingerprint verification",
|
||||
"SAS (Short Authentication String) verification",
|
||||
"Perfect Forward Secrecy with key rotation",
|
||||
"Enhanced MITM attack prevention",
|
||||
"Complete ASN.1 DER validation",
|
||||
"OID and EC point verification",
|
||||
"SPKI structure validation",
|
||||
"P2P WebRTC architecture",
|
||||
"Metadata protection",
|
||||
"100% open source code"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v4.5",
|
||||
title: "Mobile & Desktop Edition",
|
||||
status: "development",
|
||||
date: "Q2 2025",
|
||||
description: "Native apps for all platforms",
|
||||
features: [
|
||||
"PWA app for mobile",
|
||||
"Electron app for desktop",
|
||||
"Real-time notifications",
|
||||
"Automatic reconnection",
|
||||
"Battery optimization",
|
||||
"Cross-device synchronization",
|
||||
"Improved UX/UI",
|
||||
"Support for files up to 100MB"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v5.0",
|
||||
title: "Quantum-Resistant Edition",
|
||||
status: "planned",
|
||||
date: "Q4 2025",
|
||||
description: "Protection against quantum computers",
|
||||
features: [
|
||||
"Post-quantum cryptography CRYSTALS-Kyber",
|
||||
"SPHINCS+ digital signatures",
|
||||
"Hybrid scheme: classic + PQ",
|
||||
"Quantum-safe key exchange",
|
||||
"Updated hashing algorithms",
|
||||
"Migration of existing sessions",
|
||||
"Compatibility with v4.x",
|
||||
"Quantum-resistant protocols"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v5.5",
|
||||
title: "Group Communications",
|
||||
status: "planned",
|
||||
date: "Q2 2026",
|
||||
description: "Group chats with preserved privacy",
|
||||
features: [
|
||||
"P2P group connections up to 8 participants",
|
||||
"Mesh networking for groups",
|
||||
"Signal Double Ratchet for groups",
|
||||
"Anonymous groups without metadata",
|
||||
"Ephemeral groups (disappear after session)",
|
||||
"Cryptographic group administration",
|
||||
"Group member auditing"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v6.0",
|
||||
title: "Decentralized Network",
|
||||
status: "research",
|
||||
date: "2027",
|
||||
description: "Fully decentralized network",
|
||||
features: [
|
||||
"LockBit node mesh network",
|
||||
"DHT for peer discovery",
|
||||
"Built-in onion routing",
|
||||
"Tokenomics and node incentives",
|
||||
"Governance via DAO",
|
||||
"Interoperability with other networks",
|
||||
"Cross-platform compatibility",
|
||||
"Self-healing network"
|
||||
]
|
||||
},
|
||||
{
|
||||
version: "v7.0",
|
||||
title: "AI Privacy Assistant",
|
||||
status: "research",
|
||||
date: "2028+",
|
||||
description: "AI for privacy and security",
|
||||
features: [
|
||||
"Local AI threat analysis",
|
||||
"Automatic MITM detection",
|
||||
"Adaptive cryptography",
|
||||
"Personalized security recommendations",
|
||||
"Zero-knowledge machine learning",
|
||||
"Private AI assistant",
|
||||
"Predictive security",
|
||||
"Autonomous attack protection"
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
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: 'Current Version'
|
||||
};
|
||||
case 'development':
|
||||
return {
|
||||
color: 'orange',
|
||||
bgClass: 'bg-orange-500/10 border-orange-500/20',
|
||||
textClass: 'text-orange-400',
|
||||
icon: 'fas fa-code',
|
||||
label: 'In Development'
|
||||
};
|
||||
case 'planned':
|
||||
return {
|
||||
color: 'blue',
|
||||
bgClass: 'bg-blue-500/10 border-blue-500/20',
|
||||
textClass: 'text-blue-400',
|
||||
icon: 'fas fa-calendar-alt',
|
||||
label: 'Planned'
|
||||
};
|
||||
case 'research':
|
||||
return {
|
||||
color: 'purple',
|
||||
bgClass: 'bg-purple-500/10 border-purple-500/20',
|
||||
textClass: 'text-purple-400',
|
||||
icon: 'fas fa-flask',
|
||||
label: 'Research'
|
||||
};
|
||||
case 'done':
|
||||
return {
|
||||
color: 'gray',
|
||||
bgClass: 'bg-gray-500/10 border-gray-500/20',
|
||||
textClass: 'text-gray-300',
|
||||
icon: 'fas fa-flag-checkered',
|
||||
label: 'Released'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: 'gray',
|
||||
bgClass: 'bg-gray-500/10 border-gray-500/20',
|
||||
textClass: 'text-gray-400',
|
||||
icon: 'fas fa-question',
|
||||
label: 'Unknown'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
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">
|
||||
Development Roadmap
|
||||
</h3>
|
||||
<p key="subtitle" className="text-secondary max-w-2xl mx-auto mb-6">
|
||||
Evolution of SecureBit.chat : from initial development to quantum-resistant decentralized network with complete ASN.1 validation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div key="roadmap-container" className="max-w-6xl mx-auto">
|
||||
<div key="timeline" className="relative">
|
||||
{/* The line has been removed */}
|
||||
|
||||
<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">
|
||||
{/* The dots are visible only on sm and larger screens */}
|
||||
|
||||
<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"
|
||||
/>
|
||||
Key features:
|
||||
</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"
|
||||
>
|
||||
Join the future of privacy
|
||||
</h4>
|
||||
<p key="cta-description" className="text-secondary mb-6">
|
||||
SecureBit.chat grows thanks to the community. Your ideas and feedback help shape the future of secure communication with complete ASN.1 validation.
|
||||
</p>
|
||||
|
||||
<div
|
||||
key="cta-buttons"
|
||||
className="flex flex-col sm:flex-row gap-4 justify-center"
|
||||
>
|
||||
<a
|
||||
key="github-link"
|
||||
href="https://github.com/SecureBitChat/SecureBitChatBrowserExtension"
|
||||
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" />
|
||||
Feedback
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
window.Roadmap = Roadmap;
|
||||
@@ -0,0 +1,59 @@
|
||||
const SecurityFeatures = () => {
|
||||
const features = [
|
||||
{ id: 'feature1', color: '#00ff88', icon: 'fas fa-key accent-green', title: 'ECDH P-384 Key Exchange', desc: 'Military-grade elliptic curve key exchange' },
|
||||
{ id: 'feature2', color: '#a78bfa', icon: 'fas fa-user-shield accent-purple', title: 'MITM Protection', desc: 'Out-of-band verification against attacks' },
|
||||
{ id: 'feature3', color: '#ff8800', icon: 'fas fa-lock accent-orange', title: 'AES-GCM 256 Encryption', desc: 'Authenticated encryption standard' },
|
||||
{ id: 'feature4', color: '#00ffff', icon: 'fas fa-sync-alt accent-cyan', title: 'Perfect Forward Secrecy', desc: 'Automatic key rotation every 5 minutes' },
|
||||
{ id: 'feature5', color: '#0088ff', icon: 'fas fa-signature accent-blue', title: 'ECDSA P-384 Signatures', desc: 'Digital signatures for message integrity' },
|
||||
{ id: 'feature6', color: '#f87171', icon: 'fas fa-shield-alt accent-red', title: 'SAS Security', desc: 'Revolutionary key exchange & MITM protection' }
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
const cards = document.querySelectorAll(".card");
|
||||
const radius = 200;
|
||||
|
||||
const handleMove = (e) => {
|
||||
cards.forEach((card) => {
|
||||
const rect = card.getBoundingClientRect();
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
|
||||
const dx = e.clientX - cx;
|
||||
const dy = e.clientY - cy;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (dist < radius) {
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
card.style.setProperty("--x", `${x}px`);
|
||||
card.style.setProperty("--y", `${y}px`);
|
||||
card.classList.add("active-glow");
|
||||
} else {
|
||||
card.classList.remove("active-glow");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMove);
|
||||
return () => window.removeEventListener("mousemove", handleMove);
|
||||
}, []);
|
||||
|
||||
const renderFeature = (f) =>
|
||||
React.createElement('div', {
|
||||
key: f.id,
|
||||
className: "card p-3 sm:p-4 text-center",
|
||||
style: { "--color": f.color }
|
||||
}, [
|
||||
React.createElement('div', { key: 'icon', className: "w-10 h-10 sm:w-12 sm:h-12 flex items-center justify-center mx-auto mb-2 sm:mb-3 relative z-10" }, [
|
||||
React.createElement('i', { className: f.icon })
|
||||
]),
|
||||
React.createElement('h4', { key: 'title', className: "text-xs sm:text-sm font-medium text-primary mb-1 relative z-10" }, f.title),
|
||||
React.createElement('p', { key: 'desc', className: "text-xs text-muted leading-tight relative z-10" }, f.desc)
|
||||
]);
|
||||
|
||||
return React.createElement('div', {
|
||||
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"
|
||||
}, features.map(renderFeature));
|
||||
};
|
||||
|
||||
window.SecurityFeatures = SecurityFeatures;
|
||||
@@ -0,0 +1,334 @@
|
||||
// SessionTimer Component - v4.3.120 - ECDH + DTLS + SAS
|
||||
const SessionTimer = ({ timeLeft, sessionType, sessionManager, onDisconnect }) => {
|
||||
const [currentTime, setCurrentTime] = React.useState(timeLeft || 0);
|
||||
const [showExpiredMessage, setShowExpiredMessage] = React.useState(false);
|
||||
const [initialized, setInitialized] = React.useState(false);
|
||||
const [connectionBroken, setConnectionBroken] = React.useState(false);
|
||||
|
||||
|
||||
const [loggedHidden, setLoggedHidden] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer initialization skipped - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let initialTime = 0;
|
||||
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
initialTime = sessionManager.getTimeLeft();
|
||||
} else if (timeLeft && timeLeft > 0) {
|
||||
initialTime = timeLeft;
|
||||
}
|
||||
|
||||
if (initialTime <= 0) {
|
||||
setCurrentTime(0);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
setCurrentTime(0);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(true);
|
||||
return;
|
||||
}
|
||||
setCurrentTime(initialTime);
|
||||
setInitialized(true);
|
||||
setLoggedHidden(false);
|
||||
}, [sessionManager, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (timeLeft && timeLeft > 0) {
|
||||
setCurrentTime(timeLeft);
|
||||
}
|
||||
setLoggedHidden(false);
|
||||
}, [timeLeft, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentTime || currentTime <= 0 || !sessionManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (connectionBroken) {
|
||||
setCurrentTime(0);
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionManager?.hasActiveSession()) {
|
||||
const newTime = sessionManager.getTimeLeft();
|
||||
setCurrentTime(newTime);
|
||||
|
||||
if (window.DEBUG_MODE && Math.floor(Date.now() / 30000) !== Math.floor((Date.now() - 1000) / 30000)) {
|
||||
console.log('⏱️ Timer tick:', Math.floor(newTime / 1000) + 's');
|
||||
}
|
||||
|
||||
if (newTime <= 0) {
|
||||
setShowExpiredMessage(true);
|
||||
setTimeout(() => setShowExpiredMessage(false), 5000);
|
||||
clearInterval(interval);
|
||||
}
|
||||
} else {
|
||||
setCurrentTime(0);
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [initialized, currentTime, sessionManager, connectionBroken]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleSessionTimerUpdate = (event) => {
|
||||
if (connectionBroken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.detail.timeLeft && event.detail.timeLeft > 0) {
|
||||
setCurrentTime(event.detail.timeLeft);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForceHeaderUpdate = (event) => {
|
||||
if (connectionBroken) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessionManager && sessionManager.hasActiveSession()) {
|
||||
const newTime = sessionManager.getTimeLeft();
|
||||
setCurrentTime(newTime);
|
||||
} else {
|
||||
setCurrentTime(event.detail.timeLeft);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePeerDisconnect = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleNewConnection = (event) => {
|
||||
setConnectionBroken(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleConnectionCleaned = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleSessionReset = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleSessionCleanup = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
const handleDisconnected = (event) => {
|
||||
setConnectionBroken(true);
|
||||
setCurrentTime(0);
|
||||
setShowExpiredMessage(false);
|
||||
setInitialized(false);
|
||||
setLoggedHidden(false);
|
||||
};
|
||||
|
||||
document.addEventListener('session-timer-update', handleSessionTimerUpdate);
|
||||
document.addEventListener('force-header-update', handleForceHeaderUpdate);
|
||||
document.addEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.addEventListener('new-connection', handleNewConnection);
|
||||
document.addEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.addEventListener('session-reset', handleSessionReset);
|
||||
document.addEventListener('session-cleanup', handleSessionCleanup);
|
||||
document.addEventListener('disconnected', handleDisconnected);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('session-timer-update', handleSessionTimerUpdate);
|
||||
document.removeEventListener('force-header-update', handleForceHeaderUpdate);
|
||||
document.removeEventListener('peer-disconnect', handlePeerDisconnect);
|
||||
document.removeEventListener('new-connection', handleNewConnection);
|
||||
document.removeEventListener('connection-cleaned', handleConnectionCleaned);
|
||||
document.removeEventListener('session-reset', handleSessionReset);
|
||||
document.removeEventListener('session-cleanup', handleSessionCleanup);
|
||||
document.removeEventListener('disconnected', handleDisconnected);
|
||||
};
|
||||
}, [sessionManager]);
|
||||
|
||||
if (showExpiredMessage) {
|
||||
return React.createElement('div', {
|
||||
className: 'session-timer expired flex items-center space-x-2 px-3 py-1.5 rounded-lg animate-pulse',
|
||||
style: { background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%)' }
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: 'fas fa-exclamation-triangle text-red-400'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'message',
|
||||
className: 'text-red-400 text-sm font-medium'
|
||||
}, 'Session Expired!')
|
||||
]);
|
||||
}
|
||||
|
||||
if (!sessionManager) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - no sessionManager');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (connectionBroken) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - connection broken');
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!currentTime || currentTime <= 0) {
|
||||
if (!loggedHidden) {
|
||||
console.log('⏱️ SessionTimer hidden - no time left, currentTime:', currentTime);
|
||||
setLoggedHidden(true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (loggedHidden) {
|
||||
setLoggedHidden(false);
|
||||
}
|
||||
|
||||
const totalMinutes = Math.floor(currentTime / (60 * 1000));
|
||||
const totalSeconds = Math.floor(currentTime / 1000);
|
||||
|
||||
const isDemo = sessionType === 'demo';
|
||||
const isWarning = isDemo ? totalMinutes <= 2 : totalMinutes <= 10;
|
||||
const isCritical = isDemo ? totalSeconds <= 60 : totalMinutes <= 5;
|
||||
|
||||
const formatTime = (ms) => {
|
||||
const hours = Math.floor(ms / (60 * 60 * 1000));
|
||||
const minutes = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000));
|
||||
const seconds = Math.floor((ms % (60 * 1000)) / 1000);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
};
|
||||
|
||||
const getTimerStyle = () => {
|
||||
const totalDuration = sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000;
|
||||
const timeProgress = (totalDuration - currentTime) / totalDuration;
|
||||
|
||||
let backgroundColor, textColor, iconColor, iconClass, shouldPulse;
|
||||
|
||||
if (timeProgress <= 0.33) {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.15) 100%)';
|
||||
textColor = 'text-green-400';
|
||||
iconColor = 'text-green-400';
|
||||
iconClass = 'fas fa-clock';
|
||||
shouldPulse = false;
|
||||
} else if (timeProgress <= 0.66) {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(234, 179, 8, 0.15) 0%, rgba(202, 138, 4, 0.15) 100%)';
|
||||
textColor = 'text-yellow-400';
|
||||
iconColor = 'text-yellow-400';
|
||||
iconClass = 'fas fa-clock';
|
||||
shouldPulse = false;
|
||||
} else {
|
||||
backgroundColor = 'linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(220, 38, 38, 0.15) 100%)';
|
||||
textColor = 'text-red-400';
|
||||
iconColor = 'text-red-400';
|
||||
iconClass = 'fas fa-exclamation-triangle';
|
||||
shouldPulse = true;
|
||||
}
|
||||
|
||||
return { backgroundColor, textColor, iconColor, iconClass, shouldPulse };
|
||||
};
|
||||
|
||||
const timerStyle = getTimerStyle();
|
||||
|
||||
const handleTimerClick = () => {
|
||||
if (onDisconnect && typeof onDisconnect === 'function') {
|
||||
onDisconnect();
|
||||
}
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: `session-timer flex items-center space-x-2 px-3 py-1.5 rounded-lg transition-all duration-500 cursor-pointer hover:opacity-80 ${
|
||||
isDemo ? 'demo-session' : ''
|
||||
} ${timerStyle.shouldPulse ? 'animate-pulse' : ''}`,
|
||||
style: { background: timerStyle.backgroundColor },
|
||||
onClick: handleTimerClick,
|
||||
title: 'Click to disconnect and clear session'
|
||||
}, [
|
||||
React.createElement('i', {
|
||||
key: 'icon',
|
||||
className: `${timerStyle.iconClass} ${timerStyle.iconColor}`
|
||||
}),
|
||||
React.createElement('span', {
|
||||
key: 'time',
|
||||
className: `text-sm font-mono font-semibold ${timerStyle.textColor}`
|
||||
}, formatTime(currentTime)),
|
||||
React.createElement('div', {
|
||||
key: 'progress',
|
||||
className: 'ml-2 w-16 h-1 bg-gray-700 rounded-full overflow-hidden'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'progress-bar',
|
||||
className: `${timerStyle.textColor.replace('text-', 'bg-')} h-full rounded-full transition-all duration-500`,
|
||||
style: {
|
||||
width: `${Math.max(0, Math.min(100, (currentTime / (sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000)) * 100))}%`
|
||||
}
|
||||
})
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
window.SessionTimer = SessionTimer;
|
||||
|
||||
window.updateSessionTimer = (newTimeLeft, newSessionType) => {
|
||||
document.dispatchEvent(new CustomEvent('session-timer-update', {
|
||||
detail: { timeLeft: newTimeLeft, sessionType: newSessionType }
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
const Testimonials = () => {
|
||||
const testimonials = [
|
||||
{ id: "t1", rating: 5.0, text: "The interface feels modern and smooth. It saves me at least 2 hours every day when managing design tasks."},
|
||||
{ id: "t2", rating: 5.0, text: "Finally, a solution that blends speed with simplicity. My team adopted it within a week without training."},
|
||||
{ id: "t3", rating: 5.0, text: "I can track progress in real time and get a clear overview of our workflow. It feels empowering."},
|
||||
{ id: "t4", rating: 5.0, text: "Our pipeline visibility improved dramatically. I no longer need to manually track updates."},
|
||||
{ id: "t5", rating: 5.0, text: "The security-first approach gives me peace of mind. We handle sensitive data with confidence now."},
|
||||
{ id: "t6", rating: 5.0, text: "User feedback cycles are now twice as fast. It helps us test and ship features quickly."}
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
const colUp = document.querySelector(".col-up");
|
||||
const colDown = document.querySelector(".col-down");
|
||||
const wrapper = document.querySelector(".testimonials-wrapper");
|
||||
|
||||
if (!colUp || !colDown || !wrapper) return;
|
||||
|
||||
let paused = false;
|
||||
const speed = 0.5;
|
||||
let animationId;
|
||||
|
||||
const cloneCards = (container) => {
|
||||
const cards = Array.from(container.children);
|
||||
cards.forEach(card => {
|
||||
const clone = card.cloneNode(true);
|
||||
container.appendChild(clone);
|
||||
});
|
||||
};
|
||||
|
||||
cloneCards(colUp);
|
||||
cloneCards(colDown);
|
||||
|
||||
const getHalfHeight = (el) => {
|
||||
const children = Array.from(el.children);
|
||||
const halfCount = children.length / 2;
|
||||
let height = 0;
|
||||
for (let i = 0; i < halfCount; i++) {
|
||||
height += children[i].offsetHeight;
|
||||
if (i < halfCount - 1) height += 24;
|
||||
}
|
||||
return height;
|
||||
};
|
||||
|
||||
let y1 = 0;
|
||||
const maxScroll1 = getHalfHeight(colUp);
|
||||
const maxScroll2 = getHalfHeight(colDown);
|
||||
let y2 = -maxScroll2;
|
||||
|
||||
function animate() {
|
||||
if (!paused) {
|
||||
y1 -= speed;
|
||||
y2 += speed;
|
||||
|
||||
if (Math.abs(y1) >= maxScroll1) {
|
||||
y1 = 0;
|
||||
}
|
||||
|
||||
if (y2 >= 0) {
|
||||
y2 = -maxScroll2;
|
||||
}
|
||||
|
||||
colUp.style.transform = `translateY(${y1}px)`;
|
||||
colDown.style.transform = `translateY(${y2}px)`;
|
||||
}
|
||||
animationId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
animate();
|
||||
|
||||
const handleMouseEnter = () => { paused = true; };
|
||||
const handleMouseLeave = () => { paused = false; };
|
||||
|
||||
wrapper.addEventListener("mouseenter", handleMouseEnter);
|
||||
wrapper.addEventListener("mouseleave", handleMouseLeave);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animationId);
|
||||
wrapper.removeEventListener("mouseenter", handleMouseEnter);
|
||||
wrapper.removeEventListener("mouseleave", handleMouseLeave);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderCard = (t, index) => (
|
||||
<div key={`${t.id}-${index}`} className="card bg-neutral-900 rounded-xl p-5 shadow-md w-72 text-sm text-white flex-shrink-0">
|
||||
<div className="flex items-center mb-2 text-yellow-400">
|
||||
{"★".repeat(Math.floor(t.rating))}
|
||||
<span className="ml-2 text-secondary">{t.rating.toFixed(1)}</span>
|
||||
</div>
|
||||
<p className="text-secondary mb-3">{t.text}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="py-14 px-6 bg-transparent">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 max-w-7xl mx-auto items-center">
|
||||
<div className="lg:col-span-2 flex flex-col justify-center">
|
||||
<p className="text-sm text-secondary mb-2">Testimonials</p>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-white mb-4 leading-snug">
|
||||
What our users are saying
|
||||
</h2>
|
||||
<p className="text-secondary text-sm">
|
||||
We continuously listen to our community and improve every day.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-3 testimonials-wrapper flex gap-6 overflow-hidden relative h-[420px]">
|
||||
<div className="pointer-events-none absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-[#1f1f1f]/90 to-transparent z-20"></div>
|
||||
<div className="pointer-events-none absolute bottom-0 left-0 w-full h-16 bg-gradient-to-t from-[#1f1f1f]/90 to-transparent z-20"></div>
|
||||
|
||||
<div className="col-up flex flex-col gap-6">
|
||||
{testimonials.map((t, i) => renderCard(t, i))}
|
||||
</div>
|
||||
|
||||
<div className="col-down flex flex-col gap-6">
|
||||
{testimonials.map((t, i) => renderCard(t, i))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
window.Testimonials = Testimonials;
|
||||
@@ -0,0 +1,209 @@
|
||||
// Enhanced Modern Slider Component with Loading Protection
|
||||
const UniqueFeatureSlider = () => {
|
||||
const trackRef = React.useRef(null);
|
||||
const wrapRef = React.useRef(null);
|
||||
const [current, setCurrent] = React.useState(0);
|
||||
const [isReady, setIsReady] = React.useState(false);
|
||||
|
||||
const slides = [
|
||||
{
|
||||
icon: "🛡️",
|
||||
bgImage: "linear-gradient(135deg, rgb(255 107 53 / 6%) 0%, rgb(255 140 66 / 45%) 100%)",
|
||||
thumbIcon: "🔒",
|
||||
title: "18-Layer Military Security",
|
||||
description: "Revolutionary defense system with ECDH P-384 + AES-GCM 256 + ECDSA + Complete ASN.1 Validation."
|
||||
},
|
||||
{
|
||||
icon: "🌐",
|
||||
bgImage: "linear-gradient(135deg, rgb(147 51 234 / 6%) 0%, rgb(168 85 247 / 45%) 100%)",
|
||||
thumbIcon: "🔗",
|
||||
title: "Pure P2P WebRTC",
|
||||
description: "Direct peer-to-peer connections without any servers. Complete decentralization with zero infrastructure."
|
||||
},
|
||||
{
|
||||
icon: "🔄",
|
||||
bgImage: "linear-gradient(135deg, rgb(16 185 129 / 6%) 0%, rgb(52 211 153 / 45%) 100%)",
|
||||
thumbIcon: "⚡",
|
||||
title: "Perfect Forward Secrecy",
|
||||
description: "Automatic key rotation every 5 minutes. Non-extractable keys with hardware protection."
|
||||
},
|
||||
{
|
||||
icon: "🎭",
|
||||
bgImage: "linear-gradient(135deg, rgb(6 182 212 / 6%) 0%, rgb(34 211 238 / 45%) 100%)",
|
||||
thumbIcon: "🌫️",
|
||||
title: "Traffic Obfuscation",
|
||||
description: "Fake traffic generation and pattern masking make communication indistinguishable from noise."
|
||||
},
|
||||
{
|
||||
icon: "👁️",
|
||||
bgImage: "linear-gradient(135deg, rgb(37 99 235 / 6%) 0%, rgb(59 130 246 / 45%) 100%)",
|
||||
thumbIcon: "🚫",
|
||||
title: "Zero Data Collection",
|
||||
description: "No registration, no servers, no logs. Complete anonymity with instant channels."
|
||||
}
|
||||
];
|
||||
|
||||
// Проверка готовности компонента
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setIsReady(true);
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
const isMobile = () => window.matchMedia("(max-width:767px)").matches;
|
||||
|
||||
const center = React.useCallback((i) => {
|
||||
if (!trackRef.current || !wrapRef.current) return;
|
||||
const card = trackRef.current.children[i];
|
||||
if (!card) return;
|
||||
|
||||
const axis = isMobile() ? "top" : "left";
|
||||
const size = isMobile() ? "clientHeight" : "clientWidth";
|
||||
const start = isMobile() ? card.offsetTop : card.offsetLeft;
|
||||
|
||||
wrapRef.current.scrollTo({
|
||||
[axis]: start - (wrapRef.current[size] / 2 - card[size] / 2),
|
||||
behavior: "smooth"
|
||||
});
|
||||
}, []);
|
||||
|
||||
const activate = React.useCallback((i, scroll = false) => {
|
||||
if (i === current) return;
|
||||
setCurrent(i);
|
||||
if (scroll) {
|
||||
setTimeout(() => center(i), 50);
|
||||
}
|
||||
}, [current, center]);
|
||||
|
||||
const go = (step) => {
|
||||
const newIndex = Math.min(Math.max(current + step, 0), slides.length - 1);
|
||||
activate(newIndex, true);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleKeydown = (e) => {
|
||||
if (["ArrowRight", "ArrowDown"].includes(e.key)) go(1);
|
||||
if (["ArrowLeft", "ArrowUp"].includes(e.key)) go(-1);
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeydown, { passive: true });
|
||||
return () => window.removeEventListener("keydown", handleKeydown);
|
||||
}, [current]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isReady) {
|
||||
center(current);
|
||||
}
|
||||
}, [current, center, isReady]);
|
||||
// Render loading state if not ready
|
||||
if (!isReady) {
|
||||
return React.createElement('section', {
|
||||
style: {
|
||||
background: 'transparent',
|
||||
minHeight: '400px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
},
|
||||
React.createElement('div', {
|
||||
style: {
|
||||
opacity: 0.5,
|
||||
fontSize: '14px',
|
||||
color: '#fff'
|
||||
}
|
||||
}, 'Loading...')
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement('section', { style: { background: 'transparent' } }, [
|
||||
// Header
|
||||
React.createElement('div', {
|
||||
key: 'head',
|
||||
className: 'head'
|
||||
}, [
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
className: 'text-2xl sm:text-3xl font-bold text-white mb-4 leading-snug'
|
||||
}, 'Why SecureBit.chat is unique'),
|
||||
React.createElement('div', {
|
||||
key: 'controls',
|
||||
className: 'controls'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'prev',
|
||||
id: 'prev-slider',
|
||||
className: 'nav-btn',
|
||||
'aria-label': 'Prev',
|
||||
disabled: current === 0,
|
||||
onClick: () => go(-1)
|
||||
}, '‹'),
|
||||
React.createElement('button', {
|
||||
key: 'next',
|
||||
id: 'next-slider',
|
||||
className: 'nav-btn',
|
||||
'aria-label': 'Next',
|
||||
disabled: current === slides.length - 1,
|
||||
onClick: () => go(1)
|
||||
}, '›')
|
||||
])
|
||||
]),
|
||||
|
||||
// Slider
|
||||
React.createElement('div', {
|
||||
key: 'slider',
|
||||
className: 'slider',
|
||||
ref: wrapRef
|
||||
},
|
||||
React.createElement('div', {
|
||||
className: 'track',
|
||||
ref: trackRef
|
||||
}, slides.map((slide, index) =>
|
||||
React.createElement('article', {
|
||||
key: index,
|
||||
className: 'project-card',
|
||||
...(index === current ? { active: '' } : {}),
|
||||
onMouseEnter: () => {
|
||||
if (window.matchMedia("(hover:hover)").matches) {
|
||||
activate(index, true);
|
||||
}
|
||||
},
|
||||
onClick: () => activate(index, true)
|
||||
}, [
|
||||
// Background
|
||||
React.createElement('div', {
|
||||
key: 'bg',
|
||||
className: 'project-card__bg',
|
||||
style: {
|
||||
background: slide.bgImage,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
}
|
||||
}),
|
||||
|
||||
// Content
|
||||
React.createElement('div', {
|
||||
key: 'content',
|
||||
className: 'project-card__content'
|
||||
}, [
|
||||
// Text container
|
||||
React.createElement('div', { key: 'text' }, [
|
||||
React.createElement('h3', {
|
||||
key: 'title',
|
||||
className: 'project-card__title'
|
||||
}, slide.title),
|
||||
React.createElement('p', {
|
||||
key: 'desc',
|
||||
className: 'project-card__desc'
|
||||
}, slide.description)
|
||||
])
|
||||
])
|
||||
])
|
||||
))
|
||||
),
|
||||
]);
|
||||
};
|
||||
|
||||
// Export for use in your app
|
||||
window.UniqueFeatureSlider = UniqueFeatureSlider;
|
||||
Reference in New Issue
Block a user