diff --git a/index.html b/index.html index 414e062..c14fc21 100644 --- a/index.html +++ b/index.html @@ -2309,244 +2309,309 @@ } }; - // Enhanced Chat Interface with better security indicators - const EnhancedChatInterface = ({ - messages, - messageInput, - setMessageInput, - onSendMessage, - onDisconnect, - keyFingerprint, - isVerified, - chatMessagesRef, - scrollToBottom, - webrtcManager - }) => { - const [showScrollButton, setShowScrollButton] = React.useState(false); - const [showFileTransfer, setShowFileTransfer] = React.useState(false); - React.useEffect(() => { - if (chatMessagesRef.current && messages.length > 0) { - const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current; - const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; - - if (isNearBottom) { - const scrollToBottom = () => { - if (chatMessagesRef.current) { - chatMessagesRef.current.scrollTo({ - top: chatMessagesRef.current.scrollHeight, - behavior: 'smooth' - }); - } - }; - - scrollToBottom(); - setTimeout(scrollToBottom, 50); - setTimeout(scrollToBottom, 150); + const EnhancedChatInterface = ({ + messages, + messageInput, + setMessageInput, + onSendMessage, + onDisconnect, + keyFingerprint, + isVerified, + chatMessagesRef, + scrollToBottom, + webrtcManager +}) => { + const [showScrollButton, setShowScrollButton] = React.useState(false); + const [showFileTransfer, setShowFileTransfer] = React.useState(false); + + // Автоматическая прокрутка при изменении сообщений + React.useEffect(() => { + if (chatMessagesRef.current && messages.length > 0) { + const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current; + const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; + if (isNearBottom) { + const smoothScroll = () => { + if (chatMessagesRef.current) { + chatMessagesRef.current.scrollTo({ + top: chatMessagesRef.current.scrollHeight, + behavior: 'smooth' + }); } - } - }, [messages]); - - // Scroll handler to show the button - const handleScroll = () => { - if (chatMessagesRef.current) { - const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current; - const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; - setShowScrollButton(!isNearBottom); - } - }; - - // Wrapper for scrollToBottom with button state update - const handleScrollToBottom = () => { - scrollToBottom(); - setShowScrollButton(false); - }; + }; + smoothScroll(); + setTimeout(smoothScroll, 50); + setTimeout(smoothScroll, 150); + } + } + }, [messages, chatMessagesRef]); - const handleKeyPress = (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - onSendMessage(); - } - }; + // Обработчик скролла + const handleScroll = () => { + if (chatMessagesRef.current) { + const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current; + const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; + setShowScrollButton(!isNearBottom); + } + }; - return React.createElement('div', { - className: "chat-container" - }, [ - // Chat Messages Area - React.createElement('div', { - key: 'chat-body', - className: "chat-messages-area max-w-4xl mx-auto w-full p-4 relative" - }, [ - React.createElement('div', { - key: 'messages-container', - ref: chatMessagesRef, - onScroll: handleScroll, - className: "h-full overflow-y-auto space-y-3 custom-scrollbar pr-2 scroll-smooth" - }, - messages.length === 0 ? - React.createElement('div', { - key: 'empty-state', - className: "flex items-center justify-center h-full" - }, [ - React.createElement('div', { - className: "text-center max-w-md" - }, [ - React.createElement('div', { - key: 'icon', - className: "w-16 h-16 bg-green-500/10 border border-green-500/20 rounded-xl flex items-center justify-center mx-auto mb-4" - }, [ - React.createElement('i', { - className: 'fas fa-comments text-2xl accent-green' - }) - ]), - React.createElement('h3', { - key: 'title', - className: "text-lg font-medium text-primary mb-2" - }, "Secure channel is ready!"), - React.createElement('p', { - key: 'description', - className: "text-secondary text-sm mb-4" - }, "All messages are protected by modern cryptographic algorithms"), - React.createElement('div', { - key: 'features', - className: "text-left space-y-2" - }, [ - React.createElement('div', { - key: 'f1', - className: "flex items-center text-sm text-muted" - }, [ - React.createElement('i', { - className: 'fas fa-check accent-green mr-3' - }), - 'End-to-end encryption' - ]), - React.createElement('div', { - key: 'f2', - className: "flex items-center text-sm text-muted" - }, [ - React.createElement('i', { - className: 'fas fa-check accent-green mr-3' - }), - 'Protection against replay attacks' - ]), - React.createElement('div', { - key: 'f3', - className: "flex items-center text-sm text-muted" - }, [ - React.createElement('i', { - className: 'fas fa-check accent-green mr-3' - }), - 'Integrity verification' - ]), - React.createElement('div', { - key: 'f4', - className: "flex items-center text-sm text-muted" - }, [ - React.createElement('i', { - className: 'fas fa-sync-alt accent-purple mr-3' - }), - 'Perfect Forward Secrecy' - ]) - ]) - ]) - ]) : - messages.map((msg) => - React.createElement(EnhancedChatMessage, { - key: msg.id, - message: msg.message, - type: msg.type, - timestamp: msg.timestamp - }) - ) - ) - ]), - - // Scroll down button - showScrollButton && React.createElement('button', { - key: 'scroll-down-btn', - onClick: handleScrollToBottom, - className: "absolute bottom-4 right-4 w-10 h-10 bg-gray-600/80 hover:bg-gray-500/80 text-white rounded-full flex items-center justify-center transition-all duration-200 shadow-lg", - style: { zIndex: 10 } - }, [ - React.createElement('i', { - className: 'fas fa-chevron-down' - }) - ]), - React.createElement('div', { - key: 'file-section', - className: "file-transfer-section max-w-4xl mx-auto p-4 border-t border-gray-500/10" - }, [ - React.createElement('button', { - key: 'toggle-files', - onClick: () => setShowFileTransfer(!showFileTransfer), - className: `flex items-center text-sm text-secondary hover:text-primary transition-colors ${showFileTransfer ? 'mb-4' : ''}` - }, [ - React.createElement('i', { - key: 'icon', - className: `fas fa-${showFileTransfer ? 'chevron-up' : 'paperclip'} mr-2` - }), - showFileTransfer ? 'Hide file transfer' : 'Send files' - ]), + // Прокрутка вниз по кнопке + const handleScrollToBottom = () => { + scrollToBottom(); + setShowScrollButton(false); + }; - showFileTransfer && React.createElement(FileTransferComponent, { - key: 'file-transfer', - webrtcManager: webrtcManager, - isConnected: isVerified - }) - ]), - // Enhanced Chat Input Area - React.createElement('div', { - key: 'chat-input', - className: "chat-input-area border-t border-gray-500/10" - }, [ - React.createElement('div', { - key: 'input-container', - className: "max-w-4xl mx-auto p-4" - }, [ - React.createElement('div', { - key: 'input-wrapper', - className: "flex items-end space-x-3" - }, [ - React.createElement('div', { - key: 'text-area-wrapper', - className: "flex-1 relative" - }, [ - React.createElement('textarea', { - key: 'input', - value: messageInput, - onChange: (e) => setMessageInput(e.target.value), - onKeyDown: handleKeyPress, - placeholder: "Enter message to encrypt...", - rows: 2, - maxLength: 2000, - className: "w-full p-3 bg-gray-900/30 border border-gray-500/20 rounded-lg resize-none text-primary placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm" - }), - React.createElement('div', { - key: 'input-info', - className: "absolute bottom-2 right-3 flex items-center space-x-2 text-xs text-muted" - }, [ - React.createElement('span', { - key: 'counter' - }, `${messageInput.length}/2000`), - React.createElement('span', { - key: 'hint' - }, "• Enter to send") - ]) - ]), - React.createElement('button', { - key: 'send', - onClick: onSendMessage, - disabled: !messageInput.trim(), - className: "security-shield text-white p-3 rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" - }, [ - React.createElement('i', { - className: 'fas fa-paper-plane' + // Обработчик нажатия Enter + const handleKeyPress = (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + onSendMessage(); + } + }; + + // ИСПРАВЛЕНИЕ: Проверка готовности для файловых трансферов + const isFileTransferReady = () => { + if (!webrtcManager) return false; + + const connected = webrtcManager.isConnected ? webrtcManager.isConnected() : false; + const verified = webrtcManager.isVerified || false; + const hasDataChannel = webrtcManager.dataChannel && webrtcManager.dataChannel.readyState === 'open'; + + return connected && verified && hasDataChannel; + }; + + // Возврат JSX через React.createElement + return React.createElement( + 'div', + { + className: "chat-container flex flex-col", + style: { backgroundColor: '#272827', height: 'calc(100vh - 64px)' } + }, + [ + // Область сообщений + React.createElement( + 'div', + { className: "flex-1 flex flex-col overflow-hidden" }, + React.createElement( + 'div', + { className: "flex-1 max-w-4xl mx-auto w-full p-4 overflow-hidden" }, + React.createElement( + 'div', + { + ref: chatMessagesRef, + onScroll: handleScroll, + className: "h-full overflow-y-auto space-y-3 hide-scrollbar pr-2 scroll-smooth" + }, + messages.length === 0 ? + React.createElement( + 'div', + { className: "flex items-center justify-center h-full" }, + React.createElement( + 'div', + { className: "text-center max-w-md" }, + [ + React.createElement( + 'div', + { className: "w-16 h-16 bg-green-500/10 border border-green-500/20 rounded-xl flex items-center justify-center mx-auto mb-4" }, + React.createElement( + 'svg', + { className: "w-8 h-8 text-green-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, + React.createElement('path', { + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: 2, + d: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" + }) + ) + ), + React.createElement('h3', { className: "text-lg font-medium text-gray-300 mb-2" }, "Secure channel is ready!"), + React.createElement('p', { className: "text-gray-400 text-sm mb-4" }, "All messages are protected by modern cryptographic algorithms"), + React.createElement( + 'div', + { className: "text-left space-y-2" }, + [ + ['End-to-end encryption', 'M5 13l4 4L19 7'], + ['Protection against replay attacks', 'M5 13l4 4L19 7'], + ['Integrity verification', 'M5 13l4 4L19 7'], + ['Perfect Forward Secrecy', 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15'] + ].map(([text, d], i) => + React.createElement( + 'div', + { key: `f${i}`, className: "flex items-center text-sm text-gray-400" }, + [ + React.createElement( + 'svg', + { + className: `w-4 h-4 mr-3 ${i === 3 ? 'text-purple-500' : 'text-green-500'}`, + fill: "none", + stroke: "currentColor", + viewBox: "0 0 24 24" + }, + React.createElement('path', { + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: 2, + d: d + }) + ), + text + ] + ) + ) + ) + ] + ) + ) : + messages.map((msg) => + React.createElement(EnhancedChatMessage, { + key: msg.id, + message: msg.message, + type: msg.type, + timestamp: msg.timestamp }) - ]) - ]) - ]) - ]) - ]); - }; + ) + ) + ) + ), + + // Кнопка прокрутки вниз + showScrollButton && + React.createElement( + 'button', + { + onClick: handleScrollToBottom, + className: "fixed right-6 w-12 h-12 bg-green-500/20 hover:bg-green-500/30 border border-green-500/30 text-green-400 rounded-full flex items-center justify-center transition-all duration-200 shadow-lg z-50", + style: { bottom: '160px' } + }, + React.createElement( + 'svg', + { className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, + React.createElement('path', { + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: 2, + d: "M19 14l-7 7m0 0l-7-7m7 7V3" + }) + ) + ), + + // Секция передачи файлов + React.createElement( + 'div', + { + className: "flex-shrink-0 border-t border-gray-500/10", + style: { backgroundColor: '#272827' } + }, + React.createElement( + 'div', + { className: "max-w-4xl mx-auto px-4" }, + [ + React.createElement( + 'button', + { + onClick: () => setShowFileTransfer(!showFileTransfer), + className: `flex items-center text-sm text-gray-400 hover:text-gray-300 transition-colors py-4 ${showFileTransfer ? 'mb-4' : ''}` + }, + [ + React.createElement( + 'svg', + { + className: `w-4 h-4 mr-2 transform transition-transform ${showFileTransfer ? 'rotate-180' : ''}`, + fill: "none", + stroke: "currentColor", + viewBox: "0 0 24 24" + }, + showFileTransfer ? + React.createElement('path', { + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: 2, + d: "M5 15l7-7 7 7" + }) : + React.createElement('path', { + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: 2, + d: "M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" + }) + ), + showFileTransfer ? 'Hide file transfer' : 'Send files' + ] + ), + // ИСПРАВЛЕНИЕ: Используем правильный компонент + showFileTransfer && + React.createElement(window.FileTransferComponent || (() => + React.createElement('div', { + className: "p-4 text-center text-red-400" + }, 'FileTransferComponent not loaded') + ), { + webrtcManager: webrtcManager, + isConnected: isFileTransferReady() + }) + ] + ) + ), + + // Область ввода сообщений + React.createElement( + 'div', + { className: "border-t border-gray-500/10" }, + React.createElement( + 'div', + { className: "max-w-4xl mx-auto p-4" }, + React.createElement( + 'div', + { className: "flex items-stretch space-x-3" }, + [ + React.createElement( + 'div', + { className: "flex-1 relative" }, + [ + React.createElement('textarea', { + value: messageInput, + onChange: (e) => setMessageInput(e.target.value), + onKeyDown: handleKeyPress, + placeholder: "Enter message to encrypt...", + rows: 2, + maxLength: 2000, + style: { backgroundColor: '#272827' }, + className: "w-full p-3 border border-gray-600 rounded-lg resize-none text-gray-300 placeholder-gray-500 focus:border-green-500/40 focus:outline-none transition-all custom-scrollbar text-sm" + }), + React.createElement( + 'div', + { className: "absolute bottom-2 right-3 flex items-center space-x-2 text-xs text-gray-400" }, + [ + React.createElement('span', null, `${messageInput.length}/2000`), + React.createElement('span', null, "• Enter to send") + ] + ) + ] + ), + React.createElement( + 'button', + { + onClick: onSendMessage, + disabled: !messageInput.trim(), + className: "bg-green-400/20 text-green-400 p-3 rounded-lg transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center min-h-[72px]" + }, + React.createElement( + 'svg', + { className: "w-6 h-6", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" }, + React.createElement('path', { + strokeLinecap: "round", + strokeLinejoin: "round", + strokeWidth: 2, + d: "M12 19l9 2-9-18-9 18 9-2zm0 0v-8" + }) + ) + ) + ] + ) + ) + ) + ] + ); +}; + // Main Enhanced Application Component const EnhancedSecureP2PChat = () => { @@ -2670,7 +2735,7 @@ // Safe because it's a ref object and we maintain it centrally here window.webrtcManagerRef = webrtcManagerRef; - const addMessageWithAutoScroll = (message, type) => { + const addMessageWithAutoScroll = React.useCallback((message, type) => { const newMessage = { message, type, @@ -2678,17 +2743,36 @@ timestamp: Date.now() }; - setMessages(prev => [...prev, newMessage]); - - setTimeout(() => { - if (chatMessagesRef.current) { - chatMessagesRef.current.scrollTo({ - top: chatMessagesRef.current.scrollHeight, - behavior: 'smooth' - }); - } - }, 100); - }; + setMessages(prev => { + const updated = [...prev, newMessage]; + + setTimeout(() => { + if (chatMessagesRef?.current) { + const container = chatMessagesRef.current; + try { + const { scrollTop, scrollHeight, clientHeight } = container; + const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; + + if (isNearBottom || prev.length === 0) { + requestAnimationFrame(() => { + if (container && container.scrollTo) { + container.scrollTo({ + top: container.scrollHeight, + behavior: 'smooth' + }); + } + }); + } + } catch (error) { + console.warn('Scroll error:', error); + container.scrollTop = container.scrollHeight; + } + } + }, 50); + + return updated; + }); + }, []); // Update security level based on real verification const updateSecurityLevel = React.useCallback(async () => { @@ -3617,7 +3701,6 @@ keyFingerprint: keyFingerprint, isVerified: isVerified, chatMessagesRef: chatMessagesRef, - scrollToBottom: scrollToBottom, webrtcManager: webrtcManagerRef.current }) : React.createElement(EnhancedConnectionSetup, { diff --git a/src/styles/components.css b/src/styles/components.css index 7963a64..b25e972 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -5,6 +5,15 @@ flex-direction: column; } +.hide-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +.hide-scrollbar::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} + /* ============================================ */ /* FILE TRANSFER STYLES */ /* ============================================ */ @@ -13,6 +22,31 @@ margin-top: 1rem; } +@media (max-width: 768px) { + /* Используем dvh для динамической высоты на мобильных */ + .mobile-chat-height { + height: calc(100dvh - 64px) !important; + } + + /* Кнопка прокрутки на мобильном */ + .scroll-to-bottom-mobile { + bottom: 140px !important; + right: 16px !important; + width: 40px !important; + height: 40px !important; + } +} + +@media (max-width: 480px) { + .header-minimal { + height: 56px; + } + + .mobile-chat-height { + height: calc(100dvh - 56px) !important; + } +} + .file-drop-zone { border: 2px dashed #4b5563; border-radius: 12px; @@ -96,6 +130,9 @@ -webkit-backdrop-filter: blur(5px); flex-shrink: 0; height: 64px; + position: sticky; + top: 0; + z-index: 40; } .header-minimal .cursor-pointer:hover { @@ -108,11 +145,11 @@ /* The main chat container takes up the rest of the height. */ .chat-container { - display: flex; + /* display: flex; */ flex-direction: column; height: calc(100vh - 64px); /* 64px - header height */ min-height: 0; - flex: 1; + /* flex: 1; */ } .chat-messages-area {