Improved chat UX/UI:

- Fixed message auto-scroll bug when receiving new messages
- Adjusted bottom chat section integration with proper styles
- Updated bottom chat section layout and appearance
- Hidden scrollbars for better interaction and cleaner look
This commit is contained in:
lockbitchat
2025-08-20 03:53:58 -04:00
parent ebcddca40d
commit 773215264f
2 changed files with 368 additions and 248 deletions

View File

@@ -2309,244 +2309,309 @@
} }
}; };
// Enhanced Chat Interface with better security indicators const EnhancedChatInterface = ({
const EnhancedChatInterface = ({ messages,
messages, messageInput,
messageInput, setMessageInput,
setMessageInput, onSendMessage,
onSendMessage, onDisconnect,
onDisconnect, keyFingerprint,
keyFingerprint, isVerified,
isVerified, chatMessagesRef,
chatMessagesRef, scrollToBottom,
scrollToBottom, webrtcManager
webrtcManager }) => {
}) => { const [showScrollButton, setShowScrollButton] = React.useState(false);
const [showScrollButton, setShowScrollButton] = React.useState(false); const [showFileTransfer, setShowFileTransfer] = 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 = () => { React.useEffect(() => {
if (chatMessagesRef.current) { if (chatMessagesRef.current && messages.length > 0) {
chatMessagesRef.current.scrollTo({ const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
top: chatMessagesRef.current.scrollHeight, const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
behavior: 'smooth' if (isNearBottom) {
}); const smoothScroll = () => {
} if (chatMessagesRef.current) {
}; chatMessagesRef.current.scrollTo({
top: chatMessagesRef.current.scrollHeight,
scrollToBottom(); behavior: 'smooth'
setTimeout(scrollToBottom, 50); });
setTimeout(scrollToBottom, 150);
} }
} };
}, [messages]); smoothScroll();
setTimeout(smoothScroll, 50);
setTimeout(smoothScroll, 150);
}
}
}, [messages, chatMessagesRef]);
// Scroll handler to show the button // Обработчик скролла
const handleScroll = () => { const handleScroll = () => {
if (chatMessagesRef.current) { if (chatMessagesRef.current) {
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current; const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
const isNearBottom = scrollHeight - scrollTop - clientHeight < 100; const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
setShowScrollButton(!isNearBottom); setShowScrollButton(!isNearBottom);
} }
}; };
// Wrapper for scrollToBottom with button state update // Прокрутка вниз по кнопке
const handleScrollToBottom = () => { const handleScrollToBottom = () => {
scrollToBottom(); scrollToBottom();
setShowScrollButton(false); setShowScrollButton(false);
}; };
const handleKeyPress = (e) => { // Обработчик нажатия Enter
if (e.key === 'Enter' && !e.shiftKey) { const handleKeyPress = (e) => {
e.preventDefault(); if (e.key === 'Enter' && !e.shiftKey) {
onSendMessage(); e.preventDefault();
} onSendMessage();
}; }
};
return React.createElement('div', { // ИСПРАВЛЕНИЕ: Проверка готовности для файловых трансферов
className: "chat-container" const isFileTransferReady = () => {
}, [ if (!webrtcManager) return false;
// Chat Messages Area
React.createElement('div', { const connected = webrtcManager.isConnected ? webrtcManager.isConnected() : false;
key: 'chat-body', const verified = webrtcManager.isVerified || false;
className: "chat-messages-area max-w-4xl mx-auto w-full p-4 relative" const hasDataChannel = webrtcManager.dataChannel && webrtcManager.dataChannel.readyState === 'open';
}, [
React.createElement('div', { return connected && verified && hasDataChannel;
key: 'messages-container', };
ref: chatMessagesRef,
onScroll: handleScroll, // Возврат JSX через React.createElement
className: "h-full overflow-y-auto space-y-3 custom-scrollbar pr-2 scroll-smooth" 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 ? messages.length === 0 ?
React.createElement('div', { React.createElement(
key: 'empty-state', 'div',
className: "flex items-center justify-center h-full" { className: "flex items-center justify-center h-full" },
}, [ React.createElement(
React.createElement('div', { 'div',
className: "text-center max-w-md" { className: "text-center max-w-md" },
}, [ [
React.createElement('div', { React.createElement(
key: 'icon', '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" { 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(
React.createElement('i', { 'svg',
className: 'fas fa-comments text-2xl accent-green' { className: "w-8 h-8 text-green-500", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24" },
}) React.createElement('path', {
]), strokeLinecap: "round",
React.createElement('h3', { strokeLinejoin: "round",
key: 'title', strokeWidth: 2,
className: "text-lg font-medium text-primary mb-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"
}, "Secure channel is ready!"), })
React.createElement('p', { )
key: 'description', ),
className: "text-secondary text-sm mb-4" React.createElement('h3', { className: "text-lg font-medium text-gray-300 mb-2" }, "Secure channel is ready!"),
}, "All messages are protected by modern cryptographic algorithms"), React.createElement('p', { className: "text-gray-400 text-sm mb-4" }, "All messages are protected by modern cryptographic algorithms"),
React.createElement('div', { React.createElement(
key: 'features', 'div',
className: "text-left space-y-2" { className: "text-left space-y-2" },
}, [ [
React.createElement('div', { ['End-to-end encryption', 'M5 13l4 4L19 7'],
key: 'f1', ['Protection against replay attacks', 'M5 13l4 4L19 7'],
className: "flex items-center text-sm text-muted" ['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']
React.createElement('i', { ].map(([text, d], i) =>
className: 'fas fa-check accent-green mr-3' React.createElement(
}), 'div',
'End-to-end encryption' { key: `f${i}`, className: "flex items-center text-sm text-gray-400" },
]), [
React.createElement('div', { React.createElement(
key: 'f2', 'svg',
className: "flex items-center text-sm text-muted" {
}, [ className: `w-4 h-4 mr-3 ${i === 3 ? 'text-purple-500' : 'text-green-500'}`,
React.createElement('i', { fill: "none",
className: 'fas fa-check accent-green mr-3' stroke: "currentColor",
}), viewBox: "0 0 24 24"
'Protection against replay attacks' },
]), React.createElement('path', {
React.createElement('div', { strokeLinecap: "round",
key: 'f3', strokeLinejoin: "round",
className: "flex items-center text-sm text-muted" strokeWidth: 2,
}, [ d: d
React.createElement('i', { })
className: 'fas fa-check accent-green mr-3' ),
}), text
'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' messages.map((msg) =>
}), React.createElement(EnhancedChatMessage, {
'Perfect Forward Secrecy' key: msg.id,
]) message: msg.message,
]) type: msg.type,
]) timestamp: msg.timestamp
]) :
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'
]),
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'
}) })
]) )
]) )
]) )
]) ),
]);
}; // Кнопка прокрутки вниз
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 // Main Enhanced Application Component
const EnhancedSecureP2PChat = () => { const EnhancedSecureP2PChat = () => {
@@ -2670,7 +2735,7 @@
// Safe because it's a ref object and we maintain it centrally here // Safe because it's a ref object and we maintain it centrally here
window.webrtcManagerRef = webrtcManagerRef; window.webrtcManagerRef = webrtcManagerRef;
const addMessageWithAutoScroll = (message, type) => { const addMessageWithAutoScroll = React.useCallback((message, type) => {
const newMessage = { const newMessage = {
message, message,
type, type,
@@ -2678,17 +2743,36 @@
timestamp: Date.now() timestamp: Date.now()
}; };
setMessages(prev => [...prev, newMessage]); setMessages(prev => {
const updated = [...prev, newMessage];
setTimeout(() => { setTimeout(() => {
if (chatMessagesRef.current) { if (chatMessagesRef?.current) {
chatMessagesRef.current.scrollTo({ const container = chatMessagesRef.current;
top: chatMessagesRef.current.scrollHeight, try {
behavior: 'smooth' const { scrollTop, scrollHeight, clientHeight } = container;
}); const isNearBottom = scrollHeight - scrollTop - clientHeight < 100;
}
}, 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 // Update security level based on real verification
const updateSecurityLevel = React.useCallback(async () => { const updateSecurityLevel = React.useCallback(async () => {
@@ -3617,7 +3701,6 @@
keyFingerprint: keyFingerprint, keyFingerprint: keyFingerprint,
isVerified: isVerified, isVerified: isVerified,
chatMessagesRef: chatMessagesRef, chatMessagesRef: chatMessagesRef,
scrollToBottom: scrollToBottom,
webrtcManager: webrtcManagerRef.current webrtcManager: webrtcManagerRef.current
}) })
: React.createElement(EnhancedConnectionSetup, { : React.createElement(EnhancedConnectionSetup, {

View File

@@ -5,6 +5,15 @@
flex-direction: column; 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 */ /* FILE TRANSFER STYLES */
/* ============================================ */ /* ============================================ */
@@ -13,6 +22,31 @@
margin-top: 1rem; 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 { .file-drop-zone {
border: 2px dashed #4b5563; border: 2px dashed #4b5563;
border-radius: 12px; border-radius: 12px;
@@ -96,6 +130,9 @@
-webkit-backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
flex-shrink: 0; flex-shrink: 0;
height: 64px; height: 64px;
position: sticky;
top: 0;
z-index: 40;
} }
.header-minimal .cursor-pointer:hover { .header-minimal .cursor-pointer:hover {
@@ -108,11 +145,11 @@
/* The main chat container takes up the rest of the height. */ /* The main chat container takes up the rest of the height. */
.chat-container { .chat-container {
display: flex; /* display: flex; */
flex-direction: column; flex-direction: column;
height: calc(100vh - 64px); /* 64px - header height */ height: calc(100vh - 64px); /* 64px - header height */
min-height: 0; min-height: 0;
flex: 1; /* flex: 1; */
} }
.chat-messages-area { .chat-messages-area {