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:
567
index.html
567
index.html
@@ -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, {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user