From 852f4fb163d942233e11aadb4f513bbefb602800 Mon Sep 17 00:00:00 2001 From: lorsan Date: Tue, 24 Feb 2026 21:44:08 +0300 Subject: [PATCH] Fix 2 --- frontend/src/App.tsx | 89 ++++++++++-- frontend/src/index.css | 76 ++++++++++- frontend/src/pages/ChatPage.tsx | 230 ++++++++++++++++---------------- 3 files changed, 268 insertions(+), 127 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9bf626f..e8154ec 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -38,25 +38,98 @@ function App() { useEffect(() => { // Prevent default iOS behaviors - document.addEventListener('touchmove', (e) => { + const preventBounce = (e: TouchEvent) => { if ((e.target as any).closest('.overflow-y-auto') === null && (e.target as any).nodeName !== 'INPUT' && (e.target as any).nodeName !== 'TEXTAREA') { e.preventDefault(); } - }, { passive: false }); + }; + + document.addEventListener('touchmove', preventBounce, { passive: false }); - // Handle viewport resize on mobile (keyboard appears) - const handleResize = () => { - const vh = window.innerHeight * 0.01; + // Track focus state for input fields + let isInputFocused = false; + + const handleInputFocus = () => { + isInputFocused = true; + document.body.style.position = 'fixed'; + document.body.style.width = '100%'; + }; + + const handleInputBlur = () => { + isInputFocused = false; + document.body.style.position = ''; + document.body.style.width = ''; + }; + + // Attach focus/blur listeners to all inputs + const inputs = document.querySelectorAll('input, textarea'); + inputs.forEach(input => { + input.addEventListener('focus', handleInputFocus); + input.addEventListener('blur', handleInputBlur); + }); + + // Handle viewport resize and keyboard appearance using visualViewport API + const handleViewportChange = () => { + const viewport = window.visualViewport; + if (!viewport) { + // Fallback for older browsers + const windowHeight = window.innerHeight; + const vh = windowHeight * 0.01; + document.documentElement.style.setProperty('--vh', `${vh}px`); + return; + } + + // Calculate keyboard height + const windowHeight = window.innerHeight; + const viewportHeight = viewport.height; + const keyboardHeight = Math.max(0, windowHeight - viewportHeight); + + // Set CSS variables for keyboard height + document.documentElement.style.setProperty('--keyboard-height', `${keyboardHeight}px`); + + // Set standard vh variable for better compatibility + const vh = viewport.height * 0.01; document.documentElement.style.setProperty('--vh', `${vh}px`); + + // Prevent body scrolling issues + if (keyboardHeight > 0) { + document.body.style.overflow = 'hidden'; + } else if (!isInputFocused) { + document.body.style.overflow = 'auto'; + } }; - handleResize(); - window.addEventListener('resize', handleResize); + // Listen to visualViewport changes (modern browsers) + if (window.visualViewport) { + window.visualViewport.addEventListener('resize', handleViewportChange); + window.visualViewport.addEventListener('scroll', handleViewportChange); + handleViewportChange(); + } + + // Fallback for older browsers - listen to window resize + window.addEventListener('resize', handleViewportChange); + window.addEventListener('orientationchange', () => { + setTimeout(handleViewportChange, 100); + }); + + // Initial setup + handleViewportChange(); return () => { - window.removeEventListener('resize', handleResize); + document.removeEventListener('touchmove', preventBounce); + window.removeEventListener('resize', handleViewportChange); + window.removeEventListener('orientationchange', handleViewportChange); + if (window.visualViewport) { + window.visualViewport.removeEventListener('resize', handleViewportChange); + window.visualViewport.removeEventListener('scroll', handleViewportChange); + } + + inputs.forEach(input => { + input.removeEventListener('focus', handleInputFocus); + input.removeEventListener('blur', handleInputBlur); + }); }; }, []); diff --git a/frontend/src/index.css b/frontend/src/index.css index 653ef7e..e2c47cd 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -46,6 +46,8 @@ html { height: 100%; width: 100%; overflow: hidden; + --vh: 1vh; + --keyboard-height: 0px; } html, @@ -144,6 +146,19 @@ input, textarea, select { outline: 2px solid var(--accent-primary); outline-offset: 2px; } + + /* Fix for iOS input zoom and keyboard behavior */ + input[type="text"], + input[type="email"], + input[type="password"], + input[type="search"], + textarea { + font-size: 16px !important; + padding: 8px 12px; + -webkit-appearance: none; + appearance: none; + border-radius: 8px; + } } /* Prevent text zoom when rotating */ @@ -163,7 +178,7 @@ body.modal-open { position: fixed; } -/* Fix for iOS keyboard appearance */ +/* Fix for iOS keyboard appearance - prevents scrolling under keyboard */ @supports (padding: max(0px)) { body { padding-bottom: max(env(safe-area-inset-bottom), 0px); @@ -191,7 +206,7 @@ button { border: none; } -/* Input styling for mobile */ +/* Input styling for mobile - prevent zoom and unwanted behavior */ input[type="text"], input[type="email"], input[type="password"], @@ -199,6 +214,7 @@ textarea { -webkit-appearance: none; appearance: none; border-radius: 8px; + font-size: 16px; } /* Disable auto-zoom on iOS input focus */ @@ -210,7 +226,7 @@ select { font-size: 16px !important; } -/* Smooth scrolling container */ +/* Smooth scrolling container - support iOS momentum scrolling */ .overflow-y-auto { -webkit-overflow-scrolling: touch; } @@ -253,3 +269,57 @@ select { } } +/* iOS specific fixes for input fields */ +input[type="text"], +input[type="email"], +input[type="password"], +input[type="search"], +textarea { + /* Prevent iOS default styling */ + -webkit-border-radius: 8px; + border-radius: 8px; + + /* Prevent zooming on focus */ + font-size: 16px !important; + + /* Fix for iOS bug where input gets zoomed */ + max-width: 100%; + + /* Better input styling on iOS */ + -webkit-padding-start: 12px; + padding-inline-start: 12px; +} + +/* Ensure chat container doesn't overflow */ +[role="region"] { + overflow: hidden; + max-width: 100vw; +} + +/* Fix for messages container not respecting keyboard */ +.message-container { + padding-bottom: var(--keyboard-height); + transition: padding-bottom 0.15s ease-out; +} + +/* Sticky header support */ +.sticky { + position: sticky; + z-index: 40; +} + +/* Safe area support for notch and status bar */ +@supports (padding: max(0px)) { + .sticky-top { + padding-top: max(1rem, env(safe-area-inset-top)); + } +} + +/* Smooth backdrop filter transitions */ +@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) { + .sticky-header { + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + } +} + diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/ChatPage.tsx index 0ff00ac..5693a2f 100644 --- a/frontend/src/pages/ChatPage.tsx +++ b/frontend/src/pages/ChatPage.tsx @@ -521,31 +521,31 @@ export default function ChatPage() { return ( -
+
{/* Sidebar */} -
{/* Header */} -
-

+
+

AETHER

{/* Verification Banner */} {user && !user.is_verified && ( -
+
)} {/* User Profile Section */} -
-
+
+
{/* Avatar */}
-

+

{user?.display_name || user?.username}

-

+

@{user?.username}

@@ -573,61 +573,61 @@ export default function ChatPage() { {/* Settings Icon */}
{/* Action Button */} -
+
navigate('/profile')} whileTap={{ scale: 0.95 }} - className="w-full flex items-center justify-center gap-2 py-2 px-3 rounded-xl md:rounded-2xl font-inter text-xs md:text-sm font-medium transition hover:opacity-80 min-h-touch" + className="w-full flex items-center justify-center gap-2 py-2 px-3 rounded-xl font-inter text-sm font-medium transition hover:opacity-80" style={{ backgroundColor: 'var(--bg-input)', color: 'var(--accent-primary)' }} > - + Профиль
{/* New Chat Button */} -
+
setShowNewChatModal(true)} - className="w-full flex items-center justify-center gap-2 md:gap-3 py-2.5 md:py-3 px-3 md:px-4 rounded-xl md:rounded-2xl font-inter font-semibold text-sm md:text-base shadow-sm hover:shadow-md transition min-h-touch" + className="w-full flex items-center justify-center gap-3 py-3 px-4 rounded-2xl font-inter font-semibold shadow-sm hover:shadow-md transition" style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }} > - + Новый чат
{/* Chats List */} -
-
+
+
{loading ? (
-

+

Загрузка чатов...

) : error ? (
-

{error}

+

{error}

) : chats.length === 0 ? (
-

+

Пока нет чатов

@@ -641,17 +641,17 @@ export default function ChatPage() { whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} onClick={() => handleChatClick(chat)} - className="p-2.5 md:p-4 rounded-lg md:rounded-2xl cursor-pointer transition-all relative overflow-hidden min-h-touch" + className="p-4 rounded-2xl cursor-pointer transition-all relative overflow-hidden" style={{ backgroundColor: selectedChat?.chat_id === chat.chat_id ? 'var(--accent-primary)' : 'var(--bg-input)', boxShadow: selectedChat?.chat_id === chat.chat_id ? '0 4px 12px rgba(0,0,0,0.1)' : 'none', }} > -

+
{/* Chat Avatar with Online Indicator */}
{/* Online indicator */}

{chat.display_name} @@ -696,14 +696,14 @@ export default function ChatPage() { {chat.last_message ? (

{chat.last_message}

) : (

Нет сообщений @@ -718,7 +718,7 @@ export default function ChatPage() {

{/* Footer Info */} -
+

Aether Chat v1.0

@@ -726,13 +726,13 @@ export default function ChatPage() {
{/* Main Chat Area */} -
{selectedChat ? ( <> {/* Chat Header */} -
@@ -740,21 +740,21 @@ export default function ChatPage() { whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={handleBackToChats} - className="p-2 rounded-full transition min-h-touch min-w-touch" + className="p-2 rounded-full transition" style={{ backgroundColor: 'var(--bg-input)', color: 'var(--text-primary)' }} title="Назад к чатам" > - + handleViewUserProfile(selectedChat.user_id)} - className="w-10 h-10 md:w-12 md:h-12 flex-shrink-0 flex items-center justify-center cursor-pointer" + className="w-12 h-12 flex-shrink-0 flex items-center justify-center cursor-pointer" style={{ backgroundImage: selectedChat.avatar_url ? `url(${selectedChat.avatar_url})` : undefined, backgroundSize: 'cover', @@ -771,7 +771,7 @@ export default function ChatPage() { className="flex-1 min-w-0 cursor-pointer" onClick={() => handleViewUserProfile(selectedChat.user_id)} > -

+

{selectedChat.display_name}

@@ -784,7 +784,7 @@ export default function ChatPage() {

{/* Date divider */} {showDateDivider && ( -
-
+
{/* Avatar for incoming messages */} {!isMyMessage && showAvatar && ( @@ -872,7 +872,7 @@ export default function ChatPage() { initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.1 }} - className="w-7 h-7 md:w-9 md:h-9 flex-shrink-0 flex items-center justify-center self-end shadow-md" + className="w-9 h-9 flex-shrink-0 flex items-center justify-center self-end shadow-md" style={{ backgroundImage: selectedChat.avatar_url ? `url(${selectedChat.avatar_url})` : undefined, backgroundSize: 'cover', @@ -888,20 +888,20 @@ export default function ChatPage() { )} {/* Message bubble */} -
+
{/* Sender name for incoming messages */} {!isMyMessage && showAvatar && ( - + {selectedChat.display_name} )} - {/* Edit and Delete buttons - только для своих сообщений на десктопе */} + {/* Edit and Delete buttons - показываем только для своих сообщений */} {isMyMessage && editingMessageId !== message.id && ( <> )} {/* Message content or edit input */} {editingMessageId === message.id ? ( -
+
setEditingMessageText(e.target.value)} onKeyDown={(e) => handleEditKeyPress(e, message.id)} autoFocus - className="flex-1 bg-transparent border-b-2 outline-none text-sm md:text-base" + className="flex-1 bg-transparent border-b-2 outline-none" style={{ borderColor: isMyMessage ? 'rgba(255,255,255,0.5)' : 'var(--accent-primary)', color: isMyMessage ? 'white' : 'var(--text-primary)' @@ -951,17 +951,17 @@ export default function ChatPage() { />
) : ( @@ -969,11 +969,11 @@ export default function ChatPage() { )} {/* Message metadata */} -
+
{/* Edited indicator */} {message.is_edited && ( - {/* Reactions placeholder */} + {/* Reactions placeholder - можно добавить позже */} {isLastInGroup && false && (
❤️ @@ -1076,7 +1076,7 @@ export default function ChatPage() { {/* Message Input */} -
@@ -1087,14 +1087,13 @@ export default function ChatPage() { value={messageText} onChange={(e) => setMessageText(e.target.value)} onKeyPress={handleKeyPress} - placeholder="Сообщение..." + placeholder="Написать сообщение..." disabled={sendingMessage} - className="flex-1 px-3 md:px-4 py-2 md:py-3 rounded-lg md:rounded-2xl font-inter text-sm md:text-base outline-none transition min-h-touch" + className="flex-1 px-3 md:px-4 py-2.5 md:py-3 rounded-2xl font-inter text-sm outline-none transition" style={{ backgroundColor: 'var(--bg-input)', color: 'var(--text-primary)', borderBottom: '2px solid transparent', - fontSize: '16px', // Prevent iOS zoom on input focus }} onFocus={(e) => e.target.style.borderBottomColor = 'var(--accent-primary)'} onBlur={(e) => e.target.style.borderBottomColor = 'transparent'} @@ -1103,13 +1102,13 @@ export default function ChatPage() { whileTap={{ scale: 0.95 }} onClick={handleSendMessage} disabled={!messageText.trim() || sendingMessage} - className="px-3 md:px-6 py-2 md:py-3 rounded-lg md:rounded-2xl font-inter font-semibold flex items-center justify-center gap-1 md:gap-2 transition hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed min-h-touch min-w-touch" + className="px-4 md:px-6 py-2.5 md:py-3 rounded-2xl font-inter font-semibold flex items-center gap-2 transition hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed" style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }} > {sendingMessage ? ( -
+
) : ( - + )}
@@ -1146,38 +1145,38 @@ export default function ChatPage() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="fixed inset-0 bg-black/50 flex items-end md:items-center justify-center z-50 p-4" + className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={() => setViewingUser(null)} > e.stopPropagation()} > {/* Modal Header */} -
+
-

+

Профиль пользователя

{/* Modal Content */} -
+
{/* Avatar */} -
+
{!viewingUser.avatar_url && ( - + )}
{/* User Info */} -
+
-

+

{viewingUser.display_name}

-

+

@{viewingUser.username}

{viewingUser.description && ( -
-

+

+

{viewingUser.description}

)} {viewingUser.birth_day && ( -
+
🎂 {new Date(viewingUser.birth_day).toLocaleDateString('ru-RU', { @@ -1223,18 +1222,18 @@ export default function ChatPage() {
)} -
+
✉️ - {viewingUser.email} + {viewingUser.email}
{/* Actions */} -
+
setViewingUser(null)} - className="w-full py-2.5 md:py-3 px-4 rounded-lg md:rounded-2xl font-inter font-semibold transition hover:opacity-90 min-h-touch" + className="w-full py-3 px-4 rounded-2xl font-inter font-semibold transition hover:opacity-90" style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }} > Закрыть @@ -1253,7 +1252,7 @@ export default function ChatPage() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} - className="fixed inset-0 bg-black/50 flex items-end md:items-center justify-center z-50 p-4" + className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" onClick={() => { setShowNewChatModal(false); setSearchQuery(''); @@ -1261,17 +1260,17 @@ export default function ChatPage() { }} > e.stopPropagation()} - className="w-full md:max-w-md rounded-t-3xl md:rounded-3xl shadow-2xl overflow-hidden" + className="w-full max-w-md rounded-3xl shadow-2xl overflow-hidden" style={{ backgroundColor: 'var(--bg-card)' }} > {/* Modal Header */} -
-
-

+
+
+

Новый чат

@@ -1293,52 +1292,51 @@ export default function ChatPage() { onChange={(e) => handleSearchUsers(e.target.value)} placeholder="Поиск пользователей..." autoFocus - className="w-full px-3 md:px-4 py-2 md:py-3 rounded-lg md:rounded-2xl font-inter text-sm md:text-base outline-none transition" + className="w-full px-4 py-3 rounded-2xl font-inter text-sm outline-none transition" style={{ backgroundColor: 'var(--bg-input)', color: 'var(--text-primary)', borderBottom: '2px solid transparent', - fontSize: '16px', // Prevent iOS zoom }} />
{/* Search Results */} -
+
{searchLoading ? (
-

+

Поиск...

) : searchQuery.trim().length < 2 ? (
-

+

Введите имя пользователя для поиска

) : searchResults.length === 0 ? (
-

+

Пользователи не найдены

) : ( -
+
{searchResults.map((foundUser) => ( handleStartChatWithUser(foundUser)} - className="w-full flex items-center gap-2 md:gap-3 p-2.5 md:p-3 rounded-lg md:rounded-2xl transition hover:shadow-sm min-h-touch" + className="w-full flex items-center gap-3 p-3 rounded-2xl transition hover:shadow-sm" style={{ backgroundColor: 'var(--bg-input)' }} > {/* Avatar */}
{/* User Info */} -
-

+

+

{foundUser.display_name || foundUser.username}

-

+

@{foundUser.username}