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 ? (
) : 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}